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

Compare commits

..

51 Commits
1.26 ... 1.29

Author SHA1 Message Date
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
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
43 changed files with 783 additions and 111 deletions

View File

@@ -106,11 +106,17 @@ If it fails try to use `easy_install`:
sudo easy_install thefuck
```
Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)):
```bash
brew install thefuck
```
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'
```
@@ -125,12 +131,12 @@ end
Or in your Powershell `$PROFILE` on Windows:
```powershell
function fuck {
function fuck {
$fuck = $(thefuck (get-history -count 1).commandline)
if($fuck.startswith("echo")) {
$fuck.substring(5)
}
else { iex "$fuck" }
if($fuck.startswith("echo")) {
$fuck.substring(5)
}
else { iex "$fuck" }
}
```
@@ -148,19 +154,28 @@ 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` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `cd_parent` – changes `cd..` to `cd ..`;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `fix_alt_space` – replaces Alt+Space with Space character;
* `git_no_command` – fixes wrong git commands like `git brnch`;
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `has_exists_script` – prepends `./` when script/binary exists;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `no_command` – fixes wrong console commands, for example `vom/vim`;
* `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`;
* `python_command` – prepends `python` when you trying to run not executable/without `./` python script;
* `sl_ls` – changes `sl` to `ls`;
* `rm_dir` – adds `-rf` when you trying to remove directory;
* `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` – switches command from your local layout to en.
Bundled, but not enabled by default:
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
@@ -170,7 +185,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 +206,17 @@ def get_new_command(command, settings):
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default all;
* `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output.
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output.
Or via environment variables:
* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`;
* `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`;
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`.
## Developing

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.26'
VERSION = '1.29'
setup(name='thefuck',
@@ -15,6 +15,6 @@ setup(name='thefuck',
'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']})

View File

@@ -0,0 +1,49 @@
import pytest
from thefuck.types import Command
from thefuck.rules.brew_install import match, get_new_command
from thefuck.rules.brew_install import brew_formulas
@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', '',
brew_no_available_formula), None)
assert not match(Command('brew install git', '',
brew_already_installed), None)
assert not match(Command('brew install', '', 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', '',
brew_no_available_formula), None)\
== 'brew install elasticsearch'
assert get_new_command(Command('brew install aa', '',
brew_no_available_formula),
None) != 'brew install aha'

View File

@@ -0,0 +1,28 @@
import pytest
from thefuck.types import Command
from thefuck.rules.brew_unknown_command import match, get_new_command
from thefuck.rules.brew_unknown_command import brew_commands
@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', '', 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', '', brew_unknown_cmd), None)\
== 'brew list'
assert get_new_command(Command('brew instaa', '', brew_unknown_cmd_instaa),
None) == 'brew install'

View File

@@ -1,4 +1,4 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.cd_parent import match, get_new_command

View File

@@ -0,0 +1,48 @@
import pytest
from thefuck.types import Command
from thefuck.rules.composer_not_command import match, get_new_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', '', composer_not_command), None)
assert match(Command('composer pdate', '', composer_not_command_one_of_this), None)
assert not match(Command('ls update', '', 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', '', composer_not_command), None) \
== 'composer update'
assert get_new_command(
Command('composer pdate', '', composer_not_command_one_of_this), None) == 'composer selfupdate'

View File

@@ -0,0 +1,18 @@
# -*- encoding: utf-8 -*-
from thefuck.types import Command
from thefuck.rules.fix_alt_space import match, get_new_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', '', 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,5 +1,5 @@
import pytest
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.git_not_command import match, get_new_command

View File

@@ -1,5 +1,5 @@
import pytest
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.git_push import match, get_new_command

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

@@ -1,4 +1,4 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.mkdir_p import match, get_new_command

View File

@@ -0,0 +1,24 @@
import pytest
from thefuck.types import Command
from thefuck.rules.pip_unknown_command import match, get_new_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', '', pip_unknown_cmd), None)
assert not match(Command('pip i', '', pip_unknown_cmd_without_recommend),
None)
def test_get_new_command(pip_unknown_cmd):
assert get_new_command(Command('pip instatl', '', pip_unknown_cmd), None)\
== 'pip install'

View File

@@ -1,9 +1,11 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.python_command import match, get_new_command
def test_match():
assert match(Command('temp.py', '', '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'

View File

@@ -1,9 +1,10 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.rm_dir import match, get_new_command
def test_match():
assert match(Command('rm foo', '', 'rm: foo: is a directory'), None)
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)

View File

@@ -0,0 +1,18 @@
from mock import Mock
from thefuck.rules.rm_root import match, get_new_command
def test_match():
assert match(Mock(script='rm -rf /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='ls',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm --no-preserve-root /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm -rf /',
stderr=''), None)
def test_get_new_command():
assert get_new_command(Mock(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.types import Command
from thefuck.rules.sl_ls import match, get_new_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

@@ -1,7 +1,7 @@
import os
import pytest
from mock import Mock
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys

View File

@@ -1,4 +1,4 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.sudo import match, get_new_command

61
tests/test_conf.py Normal file
View File

@@ -0,0 +1,61 @@
from mock import patch, Mock
from thefuck.types import Rule
from thefuck import conf
def test_default():
assert Rule('test', None, None, True) in conf.DEFAULT_RULES
assert Rule('test', None, None, False) not in conf.DEFAULT_RULES
assert Rule('test', None, None, 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']

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,7 @@
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
def test_load_rule():
@@ -23,26 +10,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')) \
== types.Rule('bash', match, get_new_command, True)
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))) \
== [types.Rule('bash', 'bash', 'bash', True),
types.Rule('lisp', 'lisp', 'lisp', True),
types.Rule('bash', 'bash', 'bash', True),
types.Rule('lisp', 'lisp', 'lisp', True)]
assert list(main.get_rules(
Path('~'),
Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('bash', 'bash', 'bash')]
Mock(rules=types.RulesNamesList(['bash'])))) \
== [types.Rule('bash', 'bash', 'bash', True),
types.Rule('bash', 'bash', 'bash', True)]
def test_get_command():
@@ -55,7 +47,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')
== types.Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
@@ -65,24 +57,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 = [types.Rule('', lambda x, _: x.script == 'cd ..', None, True),
types.Rule('', lambda *_: False, None, True),
types.Rule('rule', Mock(side_effect=OSError('Denied')), None, True)]
assert main.get_matched_rule(types.Command('ls', '', ''),
rules, Mock(no_colors=True)) is None
assert main.get_matched_rule(types.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(types.Rule('', None, lambda *_: 'new-command', True),
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(types.Rule('', None, lambda *_: 'new-command', True),
None, None)
assert capsys.readouterr() == ('', '')
@@ -93,9 +85,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'

25
tests/test_utils.py Normal file
View File

@@ -0,0 +1,25 @@
from mock import Mock
from thefuck.utils import sudo_support, wrap_settings
from thefuck.types import Command, Settings
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', 'out', 'err'), None)
fn.assert_called_once_with(Command('ls', 'out', 'err'), None)
fn.return_value = False
assert not sudo_support(fn)(Command('sudo ls', 'out', 'err'), None)
fn.return_value = 'pwd'
assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None) == 'sudo pwd'
assert sudo_support(fn)(Command('ls', 'out', 'err'), None) == 'pwd'

94
thefuck/conf.py Normal file
View File

@@ -0,0 +1,94 @@
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'}
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)

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():
@@ -23,39 +19,25 @@ def setup_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)

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)

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]

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

@@ -5,7 +5,9 @@ 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.']
def match(command, settings):

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