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

Compare commits

..

15 Commits
1.41 ... 1.43

Author SHA1 Message Date
nvbn
d088dac0f4 Bump to 1.43 2015-05-21 01:07:41 +03:00
nvbn
c65fdd0f81 Add rule for django south inconsistent migrations 2015-05-21 00:55:23 +03:00
nvbn
e7d7b80c09 Add rule for django south ghost migrations 2015-05-21 00:49:56 +03:00
Vladimir Iakovlev
f986df23d5 Merge pull request #212 from scorphus/fix-whois
fix(whois): check if there's at least one argument to `whois`
2015-05-21 00:33:22 +03:00
Vladimir Iakovlev
cf5f18bf23 Merge pull request #211 from mcarton/man
Add a rule to change man section
2015-05-21 00:32:23 +03:00
Pablo Santiago Blum de Aguiar
44c06c483e fix(whois): check if there's at least one argument to whois
This avoids thefuck failing when there's no arguments. It fails with:

```
  ...
  File "thefuck/rules/whois.py", line 26, in get_new_command
    url = command.script.split()[1]
IndexError: list index out of range
```

Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-05-20 13:54:33 -03:00
mcarton
1f48d5e12a Add a rule to change man section 2015-05-20 18:08:15 +02:00
nvbn
2c3df1ad47 #209 add support of aliases to no_command 2015-05-20 16:58:05 +03:00
nvbn
5319871326 #209 add get_aliases to shells 2015-05-20 16:56:42 +03:00
nvbn
d6ce5e1e62 #208 .name isn't callable in specific psutil 2015-05-20 16:41:11 +03:00
Vladimir Iakovlev
c42898dde3 Merge pull request #208 from tevino/get-shell-fix
better way to get shell
2015-05-20 16:39:08 +03:00
Tevin Zhang
17b9104939 better way to get shell 2015-05-20 15:30:20 +08:00
nvbn
02d9613618 Bump to 1.42 2015-05-20 02:50:43 +03:00
nvbn
b63ce26853 Reorganize list of rules in readme 2015-05-20 02:50:08 +03:00
nvbn
ce6855fd97 Add git_pull rule 2015-05-20 02:40:36 +03:00
18 changed files with 305 additions and 57 deletions

View File

@@ -145,40 +145,49 @@ sudo pip install thefuck --upgrade
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` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `cpp11` &ndash; add missing `-std=c++11` to `g++` or `clang++`;
* `cd_parent` &ndash; changes `cd..` to `cd ..`;
* `cd_correction` &ndash; spellchecks and correct failed cd commands;
* `cd_mkdir` &ndash; creates directories before cd'ing into them;
* `cd_parent` &ndash; changes `cd..` to `cd ..`;
* `composer_not_command` &ndash; fixes composer command name;
* `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory;
* `cpp11` &ndash; add missing `-std=c++11` to `g++` or `clang++`;
* `dry` &ndash; fix repetitions like "git git push";
* `django_south_ghost` &ndash; adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` &ndash; adds `--merge` to inconsistent django south migration;
* `fix_alt_space` &ndash; replaces Alt+Space with Space character;
* `git_add` &ndash; fix *"Did you forget to 'git add'?"*;
* `git_checkout` &ndash; creates the branch before checking-out;
* `git_no_command` &ndash; fixes wrong git commands like `git brnch`;
* `git_pull` &ndash; sets upstream before executing previous `git pull`;
* `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_stash` &ndash; stashes you local modifications before rebasing or switching branch;
* `grep_recursive` &ndash; adds `-r` when you trying to grep directory;
* `has_exists_script` &ndash; prepends `./` when script/binary exists;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` &ndash; adds -lah to ls;
* `man` &ndash; change manual section;
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent;
* `no_command` &ndash; fixes wrong console commands, for example `vom/vim`;
* `no_such_file` &ndash; creates missing directories with `mv` and `cp` commands;
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
* `pacman` &ndash; installs app with `pacman` or `yaourt` if it is not installed;
* `pip_unknown_command` &ndash; fixes wrong pip commands, for example `pip instatl/pip install`;
* `python_command` &ndash; prepends `python` when you trying to run not executable/without `./` python script;
* `sl_ls` &ndash; changes `sl` to `ls`;
* `rm_dir` &ndash; adds `-rf` when you trying to remove directory;
* `sl_ls` &ndash; changes `sl` to `ls`;
* `ssh_known_hosts` &ndash; removes host from `known_hosts` on warning;
* `sudo` &ndash; prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` &ndash; switches command from your local layout to en;
* `whois` &ndash; fixes `whois` command;
* `whois` &ndash; fixes `whois` command.
Enabled by default only on specific platforms:
* `apt_get` &ndash; installs app from apt if it not installed;
* `brew_install` &ndash; fixes formula name for `brew install`;
* `composer_not_command` &ndash; fixes composer command name.
* `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `pacman` &ndash; installs app with `pacman` or `yaourt` if it is not installed.
Bundled, but not enabled by default:
* `ls_lah` &ndash; adds -lah to ls;
* `rm_root` &ndash; adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.41'
VERSION = '1.43'
setup(name='thefuck',

View File

@@ -0,0 +1,53 @@
import pytest
from thefuck.rules.django_south_ghost import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr():
return '''Traceback (most recent call last):
File "/home/nvbn/work/.../bin/python", line 42, in <module>
exec(compile(__file__f.read(), __file__, "exec"))
File "/home/nvbn/work/.../app/manage.py", line 34, in <module>
execute_from_command_line(sys.argv)
File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 443, in execute_from_command_line
utility.execute()
File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 382, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/nvbn/work/.../lib/django/core/management/base.py", line 196, in run_from_argv
self.execute(*args, **options.__dict__)
File "/home/nvbn/work/.../lib/django/core/management/base.py", line 232, in execute
output = self.handle(*args, **options)
File "/home/nvbn/work/.../app/lib/south/management/commands/migrate.py", line 108, in handle
ignore_ghosts = ignore_ghosts,
File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 193, in migrate_app
applied_all = check_migration_histories(applied_all, delete_ghosts, ignore_ghosts)
File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 88, in check_migration_histories
raise exceptions.GhostMigrations(ghosts)
south.exceptions.GhostMigrations:
! These migrations are in the database but not on disk:
<app1: 0033_auto__...>
<app1: 0034_fill_...>
<app1: 0035_rename_...>
<app2: 0003_add_...>
<app2: 0004_denormalize_...>
<app1: 0033_auto....>
<app1: 0034_fill...>
! I'm not trusting myself; either fix this yourself by fiddling
! with the south_migrationhistory table, or pass --delete-ghost-migrations
! to South to have it delete ALL of these records (this may not be good).
'''
def test_match(stderr):
assert match(Command('./manage.py migrate', stderr=stderr), None)
assert match(Command('python manage.py migrate', stderr=stderr), None)
assert not match(Command('./manage.py migrate'), None)
assert not match(Command('app migrate', stderr=stderr), None)
assert not match(Command('./manage.py test', stderr=stderr), None)
def test_get_new_command():
assert get_new_command(Command('./manage.py migrate auth'), None)\
== './manage.py migrate auth --delete-ghost-migrations'

View File

@@ -0,0 +1,43 @@
import pytest
from thefuck.rules.django_south_merge import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr():
return '''Running migrations for app:
! Migration app:0003_auto... should not have been applied before app:0002_auto__add_field_query_due_date_ but was.
Traceback (most recent call last):
File "/home/nvbn/work/.../bin/python", line 42, in <module>
exec(compile(__file__f.read(), __file__, "exec"))
File "/home/nvbn/work/.../app/manage.py", line 34, in <module>
execute_from_command_line(sys.argv)
File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 443, in execute_from_command_line
utility.execute()
File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 382, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/nvbn/work/.../lib/django/core/management/base.py", line 196, in run_from_argv
self.execute(*args, **options.__dict__)
File "/home/nvbn/work/.../lib/django/core/management/base.py", line 232, in execute
output = self.handle(*args, **options)
File "/home/nvbn/work/.../app/lib/south/management/commands/migrate.py", line 108, in handle
ignore_ghosts = ignore_ghosts,
File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 207, in migrate_app
raise exceptions.InconsistentMigrationHistory(problems)
south.exceptions.InconsistentMigrationHistory: Inconsistent migration history
The following options are available:
--merge: will just attempt the migration ignoring any potential dependency conflicts.
'''
def test_match(stderr):
assert match(Command('./manage.py migrate', stderr=stderr), None)
assert match(Command('python manage.py migrate', stderr=stderr), None)
assert not match(Command('./manage.py migrate'), None)
assert not match(Command('app migrate', stderr=stderr), None)
assert not match(Command('./manage.py test', stderr=stderr), None)
def test_get_new_command():
assert get_new_command(Command('./manage.py migrate auth'), None) \
== './manage.py migrate auth --merge'

View File

@@ -0,0 +1,29 @@
import pytest
from thefuck.rules.git_pull import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr():
return '''There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details
git pull <remote> <branch>
If you wish to set tracking information for this branch you can do so with:
git branch --set-upstream-to=<remote>/<branch> master
'''
def test_match(stderr):
assert match(Command('git pull', stderr=stderr), None)
assert not match(Command('git pull'), None)
assert not match(Command('ls', stderr=stderr), None)
def test_get_new_command(stderr):
assert get_new_command(Command('git pull', stderr=stderr), None) \
== "git branch --set-upstream-to=origin/master master && git pull"

26
tests/rules/test_man.py Normal file
View File

@@ -0,0 +1,26 @@
import pytest
from thefuck.rules.man import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command('man read'),
Command('man 2 read'),
Command('man 3 read'),
Command('man -s2 read'),
Command('man -s3 read'),
Command('man -s 2 read'),
Command('man -s 3 read')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('man read'), 'man 3 read'),
(Command('man 2 read'), 'man 3 read'),
(Command('man 3 read'), 'man 2 read'),
(Command('man -s2 read'), 'man -s3 read'),
(Command('man -s3 read'), 'man -s2 read'),
(Command('man -s 2 read'), 'man -s 3 read'),
(Command('man -s 3 read'), 'man -s 2 read')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -3,7 +3,7 @@ from thefuck.rules.no_command import match, get_new_command
def test_match():
with patch('thefuck.rules.no_command._get_all_bins',
with patch('thefuck.rules.no_command._get_all_callables',
return_value=['vim', 'apt-get']):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None)
@@ -11,7 +11,7 @@ def test_match():
def test_get_new_command():
with patch('thefuck.rules.no_command._get_all_bins',
with patch('thefuck.rules.no_command._get_all_callables',
return_value=['vim', 'apt-get']):
assert get_new_command(
Mock(stderr='vom: not found',

View File

@@ -11,6 +11,10 @@ def test_match(command):
assert match(command, None)
def test_not_match():
assert not match(Command(script='whois'), None)
@pytest.mark.parametrize('command, new_command', [
(Command('whois https://en.wikipedia.org/wiki/Main_Page'), 'whois en.wikipedia.org'),
(Command('whois https://en.wikipedia.org/'), 'whois en.wikipedia.org'),

View File

@@ -13,19 +13,33 @@ def isfile(mocker):
class TestGeneric(object):
def test_from_shell(self):
assert shells.Generic().from_shell('pwd') == 'pwd'
@pytest.fixture
def shell(self):
return shells.Generic()
def test_to_shell(self):
assert shells.Generic().to_shell('pwd') == 'pwd'
def test_from_shell(self, shell):
assert shell.from_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
assert shells.Generic().put_to_history('ls') is None
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, shell):
assert shell.put_to_history('ls') is None
assert builtins_open.call_count == 0
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@pytest.fixture
def shell(self):
return shells.Bash()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
@@ -38,42 +52,61 @@ class TestBash(object):
@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_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd'
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
shells.Bash().put_to_history('ls')
def test_put_to_history(self, builtins_open, shell):
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('ls\n')
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
@pytest.mark.usefixtures('isfile')
class TestFish(object):
@pytest.fixture
def shell(self):
return shells.Fish()
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'll')]) # Fish has no aliases but functions
def test_from_shell(self, before, after):
assert shells.Fish().from_shell(before) == after
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self):
assert shells.Fish().to_shell('pwd') == 'pwd'
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker):
def test_put_to_history(self, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shells.Fish().put_to_history('ls')
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('- cmd: ls\n when: 1430707243\n')
def test_and_(self):
assert shells.Fish().and_('foo', 'bar') == 'foo; and bar'
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
@pytest.mark.usefixtures('isfile')
class TestZsh(object):
@pytest.fixture
def shell(self):
return shells.Zsh()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
@@ -86,15 +119,23 @@ class TestZsh(object):
@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_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd'
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker):
def test_put_to_history(self, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shells.Zsh().put_to_history('ls')
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n')
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}

View File

@@ -0,0 +1,8 @@
def match(command, settings):
return 'manage.py' in command.script and \
'migrate' in command.script \
and 'or pass --delete-ghost-migrations' in command.stderr
def get_new_command(command, settings):
return u'{} --delete-ghost-migrations'.format(command.script)

View File

@@ -0,0 +1,8 @@
def match(command, settings):
return 'manage.py' in command.script and \
'migrate' in command.script \
and '--merge: will just attempt the migration' in command.stderr
def get_new_command(command, settings):
return u'{} --merge'.format(command.script)

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

@@ -0,0 +1,12 @@
def match(command, settings):
return ('git' in command.script
and 'pull' in command.script
and 'set-upstream' in command.stderr)
def get_new_command(command, settings):
line = command.stderr.split('\n')[-3].strip()
branch = line.split(' ')[-1]
set_upstream = line.replace('<remote>', 'origin')\
.replace('<branch>', branch)
return u'{} && {}'.format(set_upstream, command.script)

View File

@@ -1,5 +1,5 @@
def match(command, settings):
return (command.script.startswith('grep')
return (command.script.startswith('grep')
and 'is a directory' in command.stderr.lower())

View File

@@ -1,6 +1,3 @@
enabled_by_default = False
def match(command, settings):
return 'ls' in command.script and not ('ls -' in command.script)

13
thefuck/rules/man.py Normal file
View File

@@ -0,0 +1,13 @@
def match(command, settings):
return command.script.startswith('man')
def get_new_command(command, settings):
if '3' in command.script:
return command.script.replace("3", "2")
if '2' in command.script:
return command.script.replace("2", "3")
split_cmd = command.script.split()
split_cmd.insert(1, ' 3 ')
return "".join(split_cmd)

View File

@@ -2,6 +2,7 @@ from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support
from thefuck.shells import get_aliases
def _safe(fn, fallback):
@@ -11,25 +12,25 @@ def _safe(fn, fallback):
return fallback
def _get_all_bins():
def _get_all_callables():
return [exe.name
for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)]
if not _safe(exe.is_dir, True)] + get_aliases()
@sudo_support
def match(command, settings):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins()))
_get_all_callables()))
@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]
_get_all_callables())[0]
return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -19,7 +19,7 @@ def match(command, settings):
- www.google.fr → subdomain: www, domain: 'google.fr';
- google.co.uk → subdomain: None, domain; 'google.co.uk'.
"""
return 'whois' in command.script
return 'whois' in command.script and len(command.script.split()) > 1
def get_new_command(command, settings):

View File

@@ -1,5 +1,5 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias` and `put_to_history`
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and `get_aliases`
methods.
"""
@@ -12,11 +12,11 @@ from .utils import DEVNULL
class Generic(object):
def _get_aliases(self):
def get_aliases(self):
return {}
def _expand_aliases(self, command_script):
aliases = self._get_aliases()
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return command_script.replace(binary, aliases[binary], 1)
@@ -61,7 +61,7 @@ class Bash(Generic):
value = value[1:-1]
return name, value
def _get_aliases(self):
def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict(
self._parse_alias(alias)
@@ -111,7 +111,7 @@ class Zsh(Generic):
value = value[1:-1]
return name, value
def _get_aliases(self):
def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict(
self._parse_alias(alias)
@@ -134,7 +134,7 @@ class Tcsh(Generic):
name, value = alias.split("\t", 1)
return name, value
def _get_aliases(self):
def get_aliases(self):
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict(
self._parse_alias(alias)
@@ -153,16 +153,16 @@ shells = defaultdict(lambda: Generic(), {
'bash': Bash(),
'fish': Fish(),
'zsh': Zsh(),
'-csh': Tcsh(),
'csh': Tcsh(),
'tcsh': Tcsh()})
def _get_shell():
try:
shell = Process(os.getpid()).parent().cmdline()[0]
shell = Process(os.getpid()).parent().name()
except TypeError:
shell = Process(os.getpid()).parent.cmdline[0]
return shells[os.path.basename(shell)]
shell = Process(os.getpid()).parent.name
return shells[shell]
def from_shell(command):
@@ -183,3 +183,7 @@ def put_to_history(command):
def and_(*commands):
return _get_shell().and_(*commands)
def get_aliases():
return list(_get_shell().get_aliases().keys())