1
0
mirror of https://github.com/nvbn/thefuck.git synced 2024-10-06 10:51:11 +01:00
This commit is contained in:
Dugucloud 2015-05-10 15:46:06 +08:00
commit 7e55041963
65 changed files with 1628 additions and 259 deletions

119
README.md
View File

@ -1,5 +1,7 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck)
**Aliases changed in 1.34.**
Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320).
@ -71,7 +73,7 @@ REPL-y 0.3.1
...
```
If you are scared to blindly run changed command, there's `require_confirmation`
If you are scared to blindly run the changed command, there is a `require_confirmation`
[settings](#settings) option:
```bash
@ -88,8 +90,8 @@ Reading package lists... Done
## Requirements
- python (2.7+ or 3.3+)
- pip
- python
- python-dev
## Installation
@ -100,40 +102,30 @@ Install `The Fuck` with `pip`:
sudo pip install thefuck
```
If it fails try to use `easy_install`:
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
And add to the `.bashrc` or `.bash_profile`(for OSX):
```bash
sudo easy_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)); history -r'
# You can use whatever you want as an alias, like for Mondays:
alias FUCK='fuck'
```
Or in `config.fish`:
Or in your `.zshrc`:
```fish
function fuck
eval (thefuck $history[1])
end
```bash
alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'
```
Or in your Powershell `$PROFILE` on Windows:
Alternatively, you can redirect the output of `thefuck-alias`:
```powershell
function fuck {
$fuck = $(thefuck (get-history -count 1).commandline)
if($fuck.startswith("echo")) {
$fuck.substring(5)
}
else { iex "$fuck" }
}
```bash
thefuck-alias >> ~/.bashrc
```
[Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
Changes will be available only in a new shell session.
@ -145,21 +137,41 @@ sudo pip install thefuck --upgrade
## How it works
The Fuck tries to match rule for the previous command, create new command
using matched rule and run it. Rules enabled by default:
The Fuck tries to match a rule for the previous command, creates a new command
using the matched rule and runs it. Rules enabled by default are as follows:
* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`;
* `cd_parent` – changes `cd..` to `cd ..`;
* `cd_mkdir` – creates directories before cd'ing into them;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `dry` – fix repetitions like "git git push";
* `fix_alt_space` – replaces Alt+Space with Space character;
* `git_add` – fix *"Did you forget to 'git add'?"*;
* `git_checkout` – creates the branch before checking-out;
* `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`;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed;
* `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.
* `switch_layout` – switches command from your local layout to en;
* `apt_get` – installs app from apt if it not installed;
* `brew_install` – fixes formula name for `brew install`;
* `composer_not_command` – fixes composer command name.
Bundled, but not enabled by default:
* `ls_lah` – adds -lah to ls;
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
@ -167,10 +179,13 @@ For adding your own rule you should create `your-rule-name.py`
in `~/.thefuck/rules`. Rule should contain two functions:
`match(command: Command, settings: Settings) -> bool`
and `get_new_command(command: Command, settings: Settings) -> str`.
Also the rule can contain optional function
`side_effect(command: Command, settings: Settings) -> None` and
optional boolean `enabled_by_default`
`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`:
@ -182,6 +197,14 @@ def match(command, settings):
def get_new_command(command, settings):
return 'sudo {}'.format(command.script)
# Optional:
enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
priority = 1000 # Lower first
```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@ -189,12 +212,42 @@ def get_new_command(command, settings):
## Settings
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
The Fuck has a few settings parameters which 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`;
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – requires 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.
* `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first.
Example of `settings.py`:
```python
rules = ['sudo', 'no_command']
require_confirmation = True
wait_command = 10
no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
```
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`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first.
For example:
```bash
export THEFUCK_RULES='sudo:no_command'
export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
```
## Developing

View File

@ -28,4 +28,4 @@ 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)
call('python setup.py sdist bdist_wheel upload', shell=True)

View File

@ -1,2 +1,4 @@
pytest
mock
pytest-mock
wheel

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View File

@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.26'
VERSION = '1.39'
setup(name='thefuck',
@ -15,6 +15,7 @@ setup(name='thefuck',
'tests', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=['pathlib', 'psutil', 'colorama'],
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main']})
'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']})

0
tests/__init__.py Normal file
View File

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
from thefuck.main import Command
from thefuck.rules.cd_parent import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('cd..', '', 'cd..: command not found'), None)
assert not match(Command('', '', ''), None)
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 ..'
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'

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

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

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

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

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

@ -1,8 +1,9 @@
import os
import pytest
from mock import Mock
from thefuck.main import Command
from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys
from thefuck.rules.ssh_known_hosts import match, get_new_command,\
side_effect
from tests.utils import Command
@pytest.fixture
@ -43,27 +44,23 @@ Host key verification failed.""".format(path, '98.765.432.321')
def test_match(ssh_error):
errormsg, _, _, _ = ssh_error
assert match(Command('ssh', '', errormsg), None)
assert match(Command('ssh', '', errormsg), None)
assert match(Command('scp something something', '', errormsg), None)
assert match(Command('scp something something', '', errormsg), None)
assert not match(Command('', '', errormsg), None)
assert not match(Command('notssh', '', errormsg), None)
assert not match(Command('ssh', '', ''), None)
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):
def test_side_effect(ssh_error):
errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', '', errormsg)
remove_offending_keys(command, None)
command = Command('ssh user@host', stderr=errormsg)
side_effect(command, None)
expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n']
assert known_hosts(path) == expected
def test_get_new_command(ssh_error, monkeypatch):
errormsg, _, _, _ = ssh_error
method = Mock()
monkeypatch.setattr('thefuck.rules.ssh_known_hosts.remove_offending_keys', method)
assert get_new_command(Command('ssh user@host', '', errormsg), None) == 'ssh user@host'
assert method.call_count
assert get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host'

View File

@ -1,13 +1,21 @@
from thefuck.main import Command
import pytest
from thefuck.rules.sudo import match, get_new_command
from 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,25 +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 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='command not found: ls',
script=u'ls'), 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'
assert switch_lang.get_new_command(
Mock(script=u'λσ -λα'), None) == 'ls -la'
@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

102
tests/test_conf.py Normal file
View File

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

View File

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

78
tests/test_shells.py Normal file
View File

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

16
tests/test_types.py Normal file
View File

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

26
tests/test_utils.py Normal file
View File

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

16
tests/utils.py Normal file
View File

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

132
thefuck/conf.py Normal file
View File

@ -0,0 +1,132 @@
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_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'no_colors': False,
'priority': {}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_PRIORITY': 'priority'}
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 _priority_from_env(val):
"""Gets priority pairs from env."""
for part in val.split(':'):
try:
rule, priority = part.split('=')
yield rule, int(priority)
except ValueError:
continue
def _val_from_env(env, attr):
"""Transforms env-strings to python."""
val = os.environ[env]
if attr == 'rules':
return _rules_from_env(val)
elif attr == 'priority':
return dict(_priority_from_env(val))
elif attr == 'wait_command':
return int(val)
elif attr in ('require_confirmation', 'no_colors'):
return val.lower() == 'true'
else:
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))

View File

@ -11,28 +11,35 @@ def color(color_, settings):
return color_
def rule_failed(rule, exc_info, settings):
def exception(title, exc_info, settings):
sys.stderr.write(
u'{warn}[WARN] Rule {name}:{reset}\n{trace}'
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),
name=rule.name,
title=title,
trace=''.join(format_exception(*exc_info))))
def show_command(new_command, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format(
def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, side_effect, settings):
sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format(
command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, settings):
def confirm_command(new_command, side_effect, settings):
sys.stderr.write(
'{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format(
'{bold}{command}{side_effect}{reset} '
'[{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),

View File

@ -1,4 +1,3 @@
from collections import namedtuple
from imp import load_source
from pathlib import Path
from os.path import expanduser
@ -7,11 +6,8 @@ import os
import sys
from psutil import Process, TimeoutExpired
import colorama
from thefuck import logs
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command'))
import six
from . import logs, conf, types, shells
def setup_user_dir():
@ -20,44 +16,38 @@ 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)
settings.__dict__.setdefault('no_colors', 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),
getattr(rule_module, 'side_effect', None),
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY))
def _get_loaded_rules(rules, settings):
"""Yields all available rules."""
for rule in rules:
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
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)]
rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings)
return sorted(rules, key=lambda rule: settings.priority.get(
rule.name, rule.priority))
def wait_output(settings, popen):
@ -80,7 +70,7 @@ def wait_output(settings, popen):
def get_command(settings, args):
"""Creates command from `args` and executes it."""
if sys.version_info[0] < 3:
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else:
script = ' '.join(args[1:])
@ -88,11 +78,12 @@ def get_command(settings, args):
if not script:
return
script = shells.from_shell(script)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
if wait_output(settings, result):
return 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):
@ -105,13 +96,13 @@ def get_matched_rule(command, rules, settings):
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings):
def confirm(new_command, side_effect, settings):
"""Returns `True` when running of new command confirmed."""
if not settings.require_confirmation:
logs.show_command(new_command, settings)
logs.show_command(new_command, side_effect, settings)
return True
logs.confirm_command(new_command, settings)
logs.confirm_command(new_command, side_effect, settings)
try:
sys.stdin.read(1)
return True
@ -122,27 +113,21 @@ def confirm(new_command, settings):
def run_rule(rule, command, settings):
"""Runs command from rule for passed command."""
new_command = rule.get_new_command(command, settings)
if confirm(new_command, settings):
new_command = shells.to_shell(rule.get_new_command(command, settings))
if confirm(new_command, rule.side_effect, settings):
if rule.side_effect:
rule.side_effect(command, settings)
shells.put_to_history(new_command)
print(new_command)
def is_second_run(command):
"""Is it the second run of `fuck`?"""
return command.script.startswith('fuck')
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):
logs.failed("Can't fuck twice", settings)
return
rules = get_rules(user_dir, settings)
matched_rule = get_matched_rule(command, rules, settings)
if matched_rule:

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

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

View File

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

View File

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

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

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

View File

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

View File

@ -1,10 +1,13 @@
import re
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)

9
thefuck/rules/cpp11.py Normal file
View File

@ -0,0 +1,9 @@
def match(command, settings):
return (('g++' in command.script or 'clang++' in command.script) and
('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or
'-Wc++11-extensions' in command.stderr))
def get_new_command(command, settings):
return command.script + ' -std=c++11'

12
thefuck/rules/dry.py Normal file
View File

@ -0,0 +1,12 @@
def match(command, settings):
split_command = command.script.split()
return len(split_command) >= 2 and split_command[0] == split_command[1]
def get_new_command(command, settings):
return command.script[command.script.find(' ')+1:]
# it should be rare enough to actually have to type twice the same word, so
# this rule can have a higher priority to come before things like "cd cd foo"
priority = 900

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)

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

@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git add -- {} && {}'.format(missing_file, command.script)

View File

@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" not in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git branch {} && {}'.format(missing_file, 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

@ -0,0 +1,9 @@
def match(command, settings):
return (command.script.startswith(u'man')
and u'command not found' in command.stderr.lower())
def get_new_command(command, settings):
return u'man {}'.format(command.script[3:])
priority = 2000

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,14 +18,19 @@ 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,
_get_all_bins())[0]
return ' '.join([new_command] + command.script.split(' ')[1:])
priority = 3000

43
thefuck/rules/pacman.py Normal file
View File

@ -0,0 +1,43 @@
import subprocess
from thefuck.utils import DEVNULL
def __command_available(command):
try:
subprocess.check_output([command], stderr=DEVNULL)
return True
except subprocess.CalledProcessError:
# command exists but is not happy to be called without any argument
return True
except OSError:
return False
def __get_pkgfile(command):
try:
return subprocess.check_output(
['pkgfile', '-b', '-v', command.script.split(" ")[0]],
universal_newlines=True, stderr=subprocess.DEVNULL
).split()
except subprocess.CalledProcessError:
return None
def match(command, settings):
return 'not found' in command.stderr and __get_pkgfile(command)
def get_new_command(command, settings):
package = __get_pkgfile(command)[0]
return '{} -S {} && {}'.format(pacman, package, command.script)
if not __command_available('pkgfile'):
enabled_by_default = False
elif __command_available('yaourt'):
pacman = 'yaourt'
elif __command_available('pacman'):
pacman = 'sudo pacman'
else:
enabled_by_default = False

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

@ -22,7 +22,11 @@ def match(command, settings):
return True
def remove_offending_keys(command, settings):
def get_new_command(command, settings):
return command.script
def side_effect(command, settings):
offending = offending_pattern.findall(command.stderr)
for filepath, lineno in offending:
with open(filepath, 'r') as fh:
@ -30,8 +34,3 @@ def remove_offending_keys(command, settings):
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

@ -6,12 +6,18 @@ patterns = ['permission denied',
'Operation not permitted',
'root privilege',
'This command has to be run under the root user.',
'You need to be root to perform this command.']
'This operation requires root.',
'You need to be root to perform this command.',
'requested operation requires superuser privilege',
'must be run as root',
'must be superuser',
'Need to be root']
def match(command, settings):
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

118
thefuck/shells.py Normal file
View File

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

27
thefuck/types.py Normal file
View File

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

View File

@ -1,5 +1,10 @@
from functools import wraps
import os
import six
from .types import Command
DEVNULL = open(os.devnull, 'w')
def which(program):
@ -35,9 +40,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

6
tox.ini Normal file
View File

@ -0,0 +1,6 @@
[tox]
envlist = py27,py33,py34
[testenv]
deps = -rrequirements.txt
commands = py.test