mirror of
https://github.com/nvbn/thefuck.git
synced 2025-01-18 12:06:04 +00:00
Initial commit
This commit is contained in:
commit
71f1f4224b
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
.env
|
||||
.idea
|
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# The Fuck
|
||||
|
||||
Magnificent app which corrects your previous console command.
|
||||
|
||||
Few examples:
|
||||
|
||||
```bash
|
||||
➜ apt-get install vim
|
||||
E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
|
||||
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
|
||||
|
||||
➜ fuck
|
||||
[sudo] password for nvbn:
|
||||
Reading package lists... Done
|
||||
...
|
||||
|
||||
➜ git push
|
||||
fatal: The current branch master has no upstream branch.
|
||||
To push the current branch and set the remote as upstream, use
|
||||
|
||||
git push --set-upstream origin master
|
||||
|
||||
|
||||
➜ fuck
|
||||
Counting objects: 9, done.
|
||||
...
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Install `The Fuck`:
|
||||
|
||||
```bash
|
||||
sudo pip3 install thefuck
|
||||
```
|
||||
|
||||
And add to `.bashrc` or `.zshrc`:
|
||||
|
||||
```bash
|
||||
alias fuck='$(thefuck $(fc -ln -1))'
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Install `The Fuck` for development:
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
python3 setup.py develop
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
py.test
|
||||
```
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
pytest
|
26
setup.py
Normal file
26
setup.py
Normal file
@ -0,0 +1,26 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
version = '1'
|
||||
|
||||
setup(name='thefuck',
|
||||
version=version,
|
||||
description="",
|
||||
long_description="""\
|
||||
""",
|
||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
keywords='',
|
||||
author='',
|
||||
author_email='',
|
||||
url='',
|
||||
license='',
|
||||
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[
|
||||
# -*- Extra requirements: -*-
|
||||
],
|
||||
entry_points={'console_scripts': [
|
||||
'thefuck = thefuck.main:main',
|
||||
]},
|
||||
)
|
24
tests/rules/test_git_push.py
Normal file
24
tests/rules/test_git_push.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from thefuck.main import Command
|
||||
from thefuck.rules.git_push import match, get_new_command
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stderr():
|
||||
return '''fatal: The current branch master has no upstream branch.
|
||||
To push the current branch and set the remote as upstream, use
|
||||
|
||||
git push --set-upstream origin master
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def test_match(stderr):
|
||||
assert match(Command('git push master', '', stderr))
|
||||
assert not match(Command('git push master', '', ''))
|
||||
assert not match(Command('ls', '', stderr))
|
||||
|
||||
|
||||
def test_get_new_command(stderr):
|
||||
assert get_new_command(Command('', '', stderr))\
|
||||
== "git push --set-upstream origin master"
|
12
tests/rules/test_sudo.py
Normal file
12
tests/rules/test_sudo.py
Normal file
@ -0,0 +1,12 @@
|
||||
from thefuck.main import Command
|
||||
from thefuck.rules.sudo import match, get_new_command
|
||||
|
||||
|
||||
def test_match():
|
||||
assert match(Command('', '', 'Permission denied'))
|
||||
assert match(Command('', '', 'permission denied'))
|
||||
assert not match(Command('', '', ''))
|
||||
|
||||
|
||||
def test_get_new_command():
|
||||
assert get_new_command(Command('ls', '', '')) == 'sudo ls'
|
81
tests/test_main.py
Normal file
81
tests/test_main.py
Normal file
@ -0,0 +1,81 @@
|
||||
from unittest.mock import patch, Mock
|
||||
from subprocess import PIPE
|
||||
from pathlib import PosixPath, Path
|
||||
from thefuck import main
|
||||
|
||||
|
||||
def test_setup_user_dir():
|
||||
with patch('thefuck.main.Path.is_dir', return_value=False), \
|
||||
patch('thefuck.main.Path.mkdir') as mkdir, \
|
||||
patch('thefuck.main.Path.touch') as touch:
|
||||
main.setup_user_dir()
|
||||
assert mkdir.call_count == 2
|
||||
assert touch.call_count == 1
|
||||
with patch('thefuck.main.Path.is_dir', return_value=True), \
|
||||
patch('thefuck.main.Path.mkdir') as mkdir, \
|
||||
patch('thefuck.main.Path.touch') as touch:
|
||||
main.setup_user_dir()
|
||||
assert mkdir.call_count == 0
|
||||
assert touch.call_count == 0
|
||||
|
||||
|
||||
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(main.Settings(None), Path('bash.py'))
|
||||
assert main.is_rule_enabled(main.Settings(['bash']), Path('bash.py'))
|
||||
assert not main.is_rule_enabled(main.Settings(['bash']), Path('lisp.py'))
|
||||
|
||||
|
||||
def test_load_rule():
|
||||
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(match, get_new_command)
|
||||
load_source.assert_called_once_with('bash', '/rules/bash.py')
|
||||
|
||||
|
||||
def test_get_rules():
|
||||
with patch('thefuck.main.Path.glob') as glob, \
|
||||
patch('thefuck.main.load_source',
|
||||
lambda x, _: Mock(match=x, get_new_command=x)):
|
||||
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
|
||||
assert main.get_rules(
|
||||
Path('~'),
|
||||
main.Settings(None)) == [main.Rule('bash', 'bash'),
|
||||
main.Rule('lisp', 'lisp'),
|
||||
main.Rule('bash', 'bash'),
|
||||
main.Rule('lisp', 'lisp')]
|
||||
assert main.get_rules(
|
||||
Path('~'),
|
||||
main.Settings(['bash'])) == [main.Rule('bash', 'bash'),
|
||||
main.Rule('bash', 'bash')]
|
||||
|
||||
|
||||
def test_get_command():
|
||||
with patch('thefuck.main.Popen') as Popen:
|
||||
Popen.return_value.stdout.read.return_value = b'stdout'
|
||||
Popen.return_value.stderr.read.return_value = b'stderr'
|
||||
assert main.get_command(['thefuck', 'apt-get', 'search', 'vim']) \
|
||||
== main.Command('apt-get search vim', 'stdout', 'stderr')
|
||||
Popen.assert_called_once_with('apt-get search vim',
|
||||
shell=True,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE)
|
||||
|
||||
|
||||
def test_get_matched_rule():
|
||||
rules = [main.Rule(lambda x: x.script == 'cd ..', None),
|
||||
main.Rule(lambda _: False, None)]
|
||||
assert main.get_matched_rule(main.Command('ls', '', ''),
|
||||
rules) is None
|
||||
assert main.get_matched_rule(main.Command('cd ..', '', ''),
|
||||
rules) == rules[0]
|
85
thefuck/main.py
Normal file
85
thefuck/main.py
Normal file
@ -0,0 +1,85 @@
|
||||
from collections import namedtuple
|
||||
from imp import load_source
|
||||
from pathlib import Path
|
||||
from os.path import expanduser
|
||||
from subprocess import Popen, PIPE
|
||||
import sys
|
||||
|
||||
|
||||
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
|
||||
Settings = namedtuple('Settings', ('rules',))
|
||||
Rule = namedtuple('Rule', ('match', 'get_new_command'))
|
||||
|
||||
|
||||
def setup_user_dir() -> Path:
|
||||
"""Returns user config dir, create it when it doesn't exists."""
|
||||
user_dir = Path(expanduser('~/.thefuck'))
|
||||
if not user_dir.is_dir():
|
||||
user_dir.mkdir()
|
||||
user_dir.joinpath('rules').mkdir()
|
||||
user_dir.joinpath('settings.py').touch()
|
||||
return user_dir
|
||||
|
||||
|
||||
def get_settings(user_dir: Path) -> Settings:
|
||||
"""Returns prepared settings module."""
|
||||
settings = load_source('settings',
|
||||
str(user_dir.joinpath('settings.py')))
|
||||
return Settings(getattr(settings, 'rules', None))
|
||||
|
||||
|
||||
def is_rule_enabled(settings: Settings, rule: Path) -> bool:
|
||||
"""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: Path) -> Rule:
|
||||
"""Imports rule module and returns it."""
|
||||
rule_module = load_source(rule.name[:-3], str(rule))
|
||||
return Rule(rule_module.match, rule_module.get_new_command)
|
||||
|
||||
|
||||
def get_rules(user_dir: Path, settings: Settings) -> [Rule]:
|
||||
"""Returns all enabled rules."""
|
||||
bundled = Path(__file__).parent\
|
||||
.joinpath('rules')\
|
||||
.glob('*.py')
|
||||
user = user_dir.joinpath('rules').glob('*.py')
|
||||
return [load_rule(rule) for rule in list(bundled) + list(user)
|
||||
if is_rule_enabled(settings, rule)]
|
||||
|
||||
|
||||
def get_command(args: [str]) -> Command:
|
||||
"""Creates command from `args` and executes it."""
|
||||
script = ' '.join(args[1:])
|
||||
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
return Command(script, result.stdout.read().decode(),
|
||||
result.stderr.read().decode())
|
||||
|
||||
|
||||
def get_matched_rule(command: Command, rules: [Rule]) -> Rule:
|
||||
"""Returns first matched rule for command."""
|
||||
for rule in rules:
|
||||
if rule.match(command):
|
||||
return rule
|
||||
|
||||
|
||||
def run_rule(rule: Rule, command: Command):
|
||||
"""Runs command from rule for passed command."""
|
||||
new_command = rule.get_new_command(command)
|
||||
print(new_command)
|
||||
|
||||
|
||||
def main():
|
||||
command = get_command(sys.argv)
|
||||
user_dir = setup_user_dir()
|
||||
settings = get_settings(user_dir)
|
||||
rules = get_rules(user_dir, settings)
|
||||
matched_rule = get_matched_rule(command, rules)
|
||||
if matched_rule:
|
||||
run_rule(matched_rule, command)
|
||||
else:
|
||||
print('echo No fuck given')
|
8
thefuck/rules/git_push.py
Normal file
8
thefuck/rules/git_push.py
Normal file
@ -0,0 +1,8 @@
|
||||
def match(command):
|
||||
return ('git' in command.script
|
||||
and 'push' in command.script
|
||||
and 'set-upstream' in command.stderr)
|
||||
|
||||
|
||||
def get_new_command(command):
|
||||
return command.stderr.split('\n')[-3].strip()
|
6
thefuck/rules/sudo.py
Normal file
6
thefuck/rules/sudo.py
Normal file
@ -0,0 +1,6 @@
|
||||
def match(command):
|
||||
return 'permission denied' in command.stderr.lower()
|
||||
|
||||
|
||||
def get_new_command(command):
|
||||
return 'sudo {}'.format(command.script)
|
Loading…
Reference in New Issue
Block a user