1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-02-21 20:38:54 +00:00

Merge pull request #3 from nvbn/master

Sync with master
This commit is contained in:
秋纫 2015-05-28 09:41:18 +08:00
commit e09c6530e5
42 changed files with 953 additions and 74 deletions

View File

@ -6,4 +6,4 @@ python:
install: install:
- python setup.py develop - python setup.py develop
- pip install -r requirements.txt - pip install -r requirements.txt
script: py.test script: py.test -v

View File

@ -118,6 +118,11 @@ Or in your `.zshrc`:
alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'
``` ```
If you are using `tcsh`:
```tcsh
alias fuck 'set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`'
```
Alternatively, you can redirect the output of `thefuck-alias`: Alternatively, you can redirect the output of `thefuck-alias`:
```bash ```bash
@ -140,37 +145,50 @@ sudo pip install thefuck --upgrade
The Fuck tries to match a rule for the previous command, creates a new command 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: 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`; * `cd_correction` – spellchecks and correct failed cd commands;
* `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; * `cd_mkdir` – creates directories before cd'ing into them;
* `cd_parent` – changes `cd..` to `cd ..`;
* `composer_not_command` – fixes composer command name;
* `cp_omitting_directory` – adds `-a` when you `cp` directory; * `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`;
* `dry` – fix repetitions like "git git push"; * `dry` – fix repetitions like "git git push";
* `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` – adds `--merge` to inconsistent django south migration;
* `fix_alt_space` – replaces Alt+Space with Space character; * `fix_alt_space` – replaces Alt+Space with Space character;
* `git_add` – fix *"Did you forget to 'git add'?"*; * `git_add` – fix *"Did you forget to 'git add'?"*;
* `git_checkout` – creates the branch before checking-out; * `git_checkout` – creates the branch before checking-out;
* `git_no_command` – fixes wrong git commands like `git brnch`; * `git_no_command` – fixes wrong git commands like `git brnch`;
* `git_pull` – sets upstream before executing previous `git pull`;
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_stash` – stashes you local modifications before rebasing or switching branch;
* `grep_recursive` – adds `-r` when you trying to grep directory;
* `has_exists_script` – prepends `./` when script/binary exists; * `has_exists_script` – prepends `./` when script/binary exists;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` – adds -lah to ls;
* `man` – change manual section;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `mkdir_p` – adds `-p` when you trying to create directory without parent; * `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_command` – fixes wrong console commands, for example `vom/vim`;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `no_such_file` – creates missing directories with `mv` and `cp` commands;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed; * `open` – prepends `http` to address passed to `open`;
* `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `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; * `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; * `rm_dir` – adds `-rf` when you trying to remove directory;
* `sl_ls` – changes `sl` to `ls`;
* `ssh_known_hosts` – removes host from `known_hosts` on warning; * `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions; * `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;
* `whois` – fixes `whois` command.
Enabled by default only on specific platforms:
* `apt_get` – installs app from apt if it not installed; * `apt_get` – installs app from apt if it not installed;
* `brew_install` – fixes formula name for `brew install`; * `brew_install` – fixes formula name for `brew install`;
* `composer_not_command` – fixes composer command name. * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed.
Bundled, but not enabled by default: Bundled, but not enabled by default:
* `ls_lah` – adds -lah to ls;
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command. * `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules ## Creating your own rules

View File

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

0
tests/rules/__init__.py Normal file
View File

6
tests/rules/conftest.py Normal file
View File

@ -0,0 +1,6 @@
import pytest
@pytest.fixture(autouse=True)
def generic_shell(monkeypatch):
monkeypatch.setattr('thefuck.shells.and_', lambda *x: ' && '.join(x))

View File

@ -0,0 +1,59 @@
import pytest
from mock import Mock, patch
from thefuck.rules import apt_get
from thefuck.rules.apt_get import match, get_new_command
from tests.utils import Command
# python-commandnotfound is available in ubuntu 14.04+
@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True),
reason='Skip if python-commandnotfound is not available')
@pytest.mark.parametrize('command', [
Command(script='vim', stderr='vim: command not found')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, return_value', [
(Command(script='vim', stderr='vim: command not found'),
[('vim', 'main'), ('vim-tiny', 'main')])])
@patch('thefuck.rules.apt_get.CommandNotFound', create=True)
@patch.multiple(apt_get, create=True, apt_get='apt_get')
def test_match_mocked(cmdnf_mock, command, return_value):
get_packages = Mock(return_value=return_value)
cmdnf_mock.CommandNotFound.return_value = Mock(getPackages=get_packages)
assert match(command, None)
assert cmdnf_mock.CommandNotFound.called
assert get_packages.called
@pytest.mark.parametrize('command', [
Command(script='vim', stderr=''), Command()])
def test_not_match(command):
assert not match(command, None)
# python-commandnotfound is available in ubuntu 14.04+
@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True),
reason='Skip if python-commandnotfound is not available')
@pytest.mark.parametrize('command, new_command', [
(Command('vim'), 'sudo apt-get install vim && vim'),
(Command('convert'), 'sudo apt-get install imagemagick && convert')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command
@pytest.mark.parametrize('command, new_command, return_value', [
(Command('vim'), 'sudo apt-get install vim && vim',
[('vim', 'main'), ('vim-tiny', 'main')]),
(Command('convert'), 'sudo apt-get install imagemagick && convert',
[('imagemagick', 'main'),
('graphicsmagick-imagemagick-compat', 'universe')])])
@patch('thefuck.rules.apt_get.CommandNotFound', create=True)
@patch.multiple(apt_get, create=True, apt_get='apt_get')
def test_get_new_command_mocked(cmdnf_mock, command, new_command, return_value):
get_packages = Mock(return_value=return_value)
cmdnf_mock.CommandNotFound.return_value = Mock(getPackages=get_packages)
assert get_new_command(command, None) == new_command
assert cmdnf_mock.CommandNotFound.called
assert get_packages.called

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,39 @@
import pytest
from thefuck.rules.git_add import match, get_new_command
from tests.utils import Command
@pytest.fixture
def did_not_match(target, did_you_forget=True):
error = ("error: pathspec '{}' did not match any "
"file(s) known to git.".format(target))
if did_you_forget:
error = ("{}\nDid you forget to 'git add'?'".format(error))
return error
@pytest.mark.parametrize('command', [
Command(script='git submodule update unknown',
stderr=did_not_match('unknown')),
Command(script='git commit unknown',
stderr=did_not_match('unknown'))]) # Older versions of Git
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git submodule update known', stderr=('')),
Command(script='git commit known', stderr=('')),
Command(script='git commit unknown', # Newer versions of Git
stderr=did_not_match('unknown', False))])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('git submodule update unknown', stderr=did_not_match('unknown')),
'git add -- unknown && git submodule update unknown'),
(Command('git commit unknown', stderr=did_not_match('unknown')), # Old Git
'git add -- unknown && git commit unknown')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -0,0 +1,37 @@
import pytest
from thefuck.rules.git_checkout import match, get_new_command
from tests.utils import Command
@pytest.fixture
def did_not_match(target, did_you_forget=False):
error = ("error: pathspec '{}' did not match any "
"file(s) known to git.".format(target))
if did_you_forget:
error = ("{}\nDid you forget to 'git add'?'".format(error))
return error
@pytest.mark.parametrize('command', [
Command(script='git checkout unknown', stderr=did_not_match('unknown')),
Command(script='git commit unknown', stderr=did_not_match('unknown'))])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git submodule update unknown',
stderr=did_not_match('unknown', True)),
Command(script='git checkout known', stderr=('')),
Command(script='git commit known', stderr=(''))])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='git checkout unknown', stderr=did_not_match('unknown')),
'git branch unknown && git checkout unknown'),
(Command('git commit unknown', stderr=did_not_match('unknown')),
'git branch unknown && git commit unknown')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

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"

View File

@ -0,0 +1,39 @@
import pytest
from thefuck.rules.git_stash import match, get_new_command
from tests.utils import Command
@pytest.fixture
def cherry_pick_error():
return ('error: Your local changes would be overwritten by cherry-pick.\n'
'hint: Commit your changes or stash them to proceed.\n'
'fatal: cherry-pick failed')
@pytest.fixture
def rebase_error():
return ('Cannot rebase: Your index contains uncommitted changes.\n'
'Please commit or stash them.')
@pytest.mark.parametrize('command', [
Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error()),
Command(script='git rebase -i HEAD~7', stderr=rebase_error())])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git cherry-pick a1b2c3d', stderr=('')),
Command(script='git rebase -i HEAD~7', stderr=(''))])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error),
'git stash && git cherry-pick a1b2c3d'),
(Command('git rebase -i HEAD~7', stderr=rebase_error),
'git stash && git rebase -i HEAD~7')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -0,0 +1,12 @@
from thefuck.rules.grep_recursive import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('grep blah .', stderr='grep: .: Is a directory'), None)
assert not match(Command(), None)
def test_get_new_command():
assert get_new_command(
Command('grep blah .'), None) == 'grep -r blah .'

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

@ -0,0 +1,34 @@
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', [
Command('man'),
Command('man ')])
def test_not_match(command):
assert not 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(): 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']): return_value=['vim', 'apt-get']):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None) assert match(Mock(stderr='vom: not found', script='vom file.py'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None) assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None)
@ -11,7 +11,7 @@ def test_match():
def test_get_new_command(): 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']): return_value=['vim', 'apt-get']):
assert get_new_command( assert get_new_command(
Mock(stderr='vom: not found', Mock(stderr='vom: not found',

View File

@ -0,0 +1,19 @@
import pytest
from thefuck.rules.no_such_file import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"),
Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"),
])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'),
(Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

25
tests/rules/test_open.py Normal file
View File

@ -0,0 +1,25 @@
import pytest
from thefuck.rules.open import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='open foo.com'),
Command(script='open foo.ly'),
Command(script='open foo.org'),
Command(script='open foo.net'),
Command(script='open foo.se'),
Command(script='open foo.io')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('open foo.com'), 'open http://foo.com'),
(Command('open foo.ly'), 'open http://foo.ly'),
(Command('open foo.org'), 'open http://foo.org'),
(Command('open foo.net'), 'open http://foo.net'),
(Command('open foo.se'), 'open http://foo.se'),
(Command('open foo.io'), 'open http://foo.io')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -0,0 +1,66 @@
import pytest
from mock import patch
from thefuck.rules import pacman
from thefuck.rules.pacman import match, get_new_command
from tests.utils import Command
pacman_cmd = getattr(pacman, 'pacman', 'pacman')
PKGFILE_OUTPUT_CONVERT = '''
extra/imagemagick 6.9.1.0-1\t/usr/bin/convert
'''
PKGFILE_OUTPUT_VIM = '''
extra/gvim 7.4.712-1 \t/usr/bin/vim
extra/gvim-python3 7.4.712-1\t/usr/bin/vim
extra/vim 7.4.712-1 \t/usr/bin/vim
extra/vim-minimal 7.4.712-1 \t/usr/bin/vim
extra/vim-python3 7.4.712-1 \t/usr/bin/vim
'''
@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True),
reason='Skip if pacman is not available')
@pytest.mark.parametrize('command', [
Command(script='vim', stderr='vim: command not found')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, return_value', [
(Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM)])
@patch('thefuck.rules.pacman.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_match_mocked(subp_mock, command, return_value):
subp_mock.check_output.return_value = return_value
assert match(command, None)
assert subp_mock.check_output.called
@pytest.mark.parametrize('command', [
Command(script='vim', stderr=''), Command()])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True),
reason='Skip if pacman is not available')
@pytest.mark.parametrize('command, new_command', [
(Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd)),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd))])
def test_get_new_command(command, new_command, mocker):
assert get_new_command(command, None) == new_command
@pytest.mark.parametrize('command, new_command, return_value', [
(Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd),
PKGFILE_OUTPUT_VIM),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd),
PKGFILE_OUTPUT_CONVERT)])
@patch('thefuck.rules.pacman.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_get_new_command_mocked(subp_mock, command, new_command, return_value):
subp_mock.check_output.return_value = return_value
assert get_new_command(command, None) == new_command
assert subp_mock.check_output.called

23
tests/rules/test_whois.py Normal file
View File

@ -0,0 +1,23 @@
import pytest
from thefuck.rules.whois import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='whois https://en.wikipedia.org/wiki/Main_Page'),
Command(script='whois https://en.wikipedia.org/'),
Command(script='whois en.wikipedia.org')])
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'),
(Command('whois en.wikipedia.org'), 'whois wikipedia.org')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -13,19 +13,33 @@ def isfile(mocker):
class TestGeneric(object): class TestGeneric(object):
def test_from_shell(self): @pytest.fixture
assert shells.Generic().from_shell('pwd') == 'pwd' def shell(self):
return shells.Generic()
def test_to_shell(self): def test_from_shell(self, shell):
assert shells.Generic().to_shell('pwd') == 'pwd' assert shell.from_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open): def test_to_shell(self, shell):
assert shells.Generic().put_to_history('ls') is None 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 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') @pytest.mark.usefixtures('isfile')
class TestBash(object): class TestBash(object):
@pytest.fixture
def shell(self):
return shells.Bash()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, mocker): def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen') mock = mocker.patch('thefuck.shells.Popen')
@ -38,20 +52,69 @@ class TestBash(object):
@pytest.mark.parametrize('before, after', [ @pytest.mark.parametrize('before, after', [
('pwd', 'pwd'), ('pwd', 'pwd'),
('ll', 'ls -alF')]) ('ll', 'ls -alF')])
def test_from_shell(self, before, after): def test_from_shell(self, before, after, shell):
assert shells.Bash().from_shell(before) == after assert shell.from_shell(before) == after
def test_to_shell(self): def test_to_shell(self, shell):
assert shells.Bash().to_shell('pwd') == 'pwd' assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open): def test_put_to_history(self, builtins_open, shell):
shells.Bash().put_to_history('ls') shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('ls\n') 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.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (b'funced\nfuncsave\ngrep')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'll')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
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, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'funced': 'funced',
'funcsave': 'funcsave',
'grep': 'grep'}
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestZsh(object): class TestZsh(object):
@pytest.fixture
def shell(self):
return shells.Zsh()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, mocker): def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen') mock = mocker.patch('thefuck.shells.Popen')
@ -64,15 +127,23 @@ class TestZsh(object):
@pytest.mark.parametrize('before, after', [ @pytest.mark.parametrize('before, after', [
('pwd', 'pwd'), ('pwd', 'pwd'),
('ll', 'ls -alF')]) ('ll', 'ls -alF')])
def test_from_shell(self, before, after): def test_from_shell(self, before, after, shell):
assert shells.Zsh().from_shell(before) == after assert shell.from_shell(before) == after
def test_to_shell(self): def test_to_shell(self, shell):
assert shells.Zsh().to_shell('pwd') == 'pwd' 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', mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463) return_value=1430707243.3517463)
shells.Zsh().put_to_history('ls') shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n') 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

@ -1,6 +1,6 @@
import pytest import pytest
from mock import Mock from mock import Mock
from thefuck.utils import sudo_support, wrap_settings from thefuck.utils import sudo_support, wrap_settings, memoize
from thefuck.types import Settings from thefuck.types import Settings
from tests.utils import Command from tests.utils import Command
@ -24,3 +24,11 @@ def test_sudo_support(return_value, command, called, result):
fn = Mock(return_value=return_value, __name__='') fn = Mock(return_value=return_value, __name__='')
assert sudo_support(fn)(Command(command), None) == result assert sudo_support(fn)(Command(command), None) == result
fn.assert_called_once_with(Command(called), None) fn.assert_called_once_with(Command(called), None)
def test_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
fn.assert_called_once_with()

View File

@ -1,3 +1,5 @@
from thefuck import shells
try: try:
import CommandNotFound import CommandNotFound
except ImportError: except ImportError:
@ -20,4 +22,5 @@ def get_new_command(command, settings):
c = CommandNotFound.CommandNotFound() c = CommandNotFound.CommandNotFound()
pkgs = c.getPackages(command.script.split(" ")[0]) pkgs = c.getPackages(command.script.split(" ")[0])
name, _ = pkgs[0] name, _ = pkgs[0]
return "sudo apt-get install {} && {}".format(name, command.script) formatme = shells.and_('sudo apt-get install {}', '{}')
return formatme.format(name, command.script)

View File

@ -3,12 +3,11 @@ import os
import re import re
from subprocess import check_output from subprocess import check_output
import thefuck.logs
# Formulars are base on each local system's status # Formulars are base on each local system's status
brew_formulas = [] brew_formulas = []
try: try:
brew_path_prefix = check_output(['brew', '--prefix']).strip() brew_path_prefix = check_output(['brew', '--prefix'],
universal_newlines=True).strip()
brew_formula_path = brew_path_prefix + '/Library/Formula' brew_formula_path = brew_path_prefix + '/Library/Formula'
for file_name in os.listdir(brew_formula_path): for file_name in os.listdir(brew_formula_path):

View File

@ -12,7 +12,8 @@ TAP_CMD_PATH = '/%s/%s/cmd'
def _get_brew_path_prefix(): def _get_brew_path_prefix():
"""To get brew path""" """To get brew path"""
try: try:
return subprocess.check_output(['brew', '--prefix']).strip() return subprocess.check_output(['brew', '--prefix'],
universal_newlines=True).strip()
except: except:
return None return None

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python
__author__ = "mmussomele"
"""Attempts to spellcheck and correct failed cd commands"""
import os
from difflib import get_close_matches
from thefuck.utils import sudo_support
from thefuck.rules import cd_mkdir
MAX_ALLOWED_DIFF = 0.6
def _get_sub_dirs(parent):
"""Returns a list of the child directories of the given parent directory"""
return [child for child in os.listdir(parent) if os.path.isdir(os.path.join(parent, child))]
@sudo_support
def match(command, settings):
"""Match function copied from cd_mkdir.py"""
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):
"""
Attempt to rebuild the path string by spellchecking the directories.
If it fails (i.e. no directories are a close enough match), then it
defaults to the rules of cd_mkdir.
Change sensitivity by changing MAX_ALLOWED_DIFF. Default value is 0.6
"""
dest = command.script.split()[1].split(os.sep)
if dest[-1] == '':
dest = dest[:-1]
cwd = os.getcwd()
for directory in dest:
if directory == ".":
continue
elif directory == "..":
cwd = os.path.split(cwd)[0]
continue
best_matches = get_close_matches(directory, _get_sub_dirs(cwd), cutoff=MAX_ALLOWED_DIFF)
if best_matches:
cwd = os.path.join(cwd, best_matches[0])
else:
return cd_mkdir.get_new_command(command, settings)
return "cd {0}".format(cwd)
enabled_by_default = True

View File

@ -1,4 +1,5 @@
import re import re
from thefuck import shells
from thefuck.utils import sudo_support from thefuck.utils import sudo_support
@ -11,4 +12,5 @@ def match(command, settings):
@sudo_support @sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return re.sub(r'^cd (.*)', 'mkdir -p \\1 && cd \\1', command.script) repl = shells.and_('mkdir -p \\1', 'cd \\1')
return re.sub(r'^cd (.*)', repl, command.script)

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)

View File

@ -1,4 +1,5 @@
import re import re
from thefuck import shells
def match(command, settings): def match(command, settings):
@ -12,4 +13,5 @@ def get_new_command(command, settings):
r"error: pathspec '([^']*)' " r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0] "did not match any file\(s\) known to git.", command.stderr)[0]
return 'git add -- {} && {}'.format(missing_file, command.script) formatme = shells.and_('git add -- {}', '{}')
return formatme.format(missing_file, command.script)

View File

@ -1,4 +1,5 @@
import re import re
from thefuck import shells
def match(command, settings): def match(command, settings):
@ -12,4 +13,5 @@ def get_new_command(command, settings):
r"error: pathspec '([^']*)' " r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0] "did not match any file\(s\) known to git.", command.stderr)[0]
return 'git branch {} && {}'.format(missing_file, command.script) formatme = shells.and_('git branch {}', '{}')
return formatme.format(missing_file, 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

@ -0,0 +1,12 @@
from thefuck import shells
def match(command, settings):
# catches "Please commit or stash them" and "Please, commit your changes or
# stash them before you can switch branches."
return 'git' in command.script and 'or stash them' in command.stderr
def get_new_command(command, settings):
formatme = shells.and_('git stash', '{}')
return formatme.format(command.script)

View File

@ -0,0 +1,7 @@
def match(command, settings):
return (command.script.startswith('grep')
and 'is a directory' in command.stderr.lower())
def get_new_command(command, settings):
return 'grep -r {}'.format(command.script[5:])

View File

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

View File

@ -0,0 +1,30 @@
import re
from thefuck import shells
patterns = (
r"mv: cannot move '[^']*' to '([^']*)': No such file or directory",
r"mv: cannot move '[^']*' to '([^']*)': Not a directory",
r"cp: cannot create regular file '([^']*)': No such file or directory",
r"cp: cannot create regular file '([^']*)': Not a directory",
)
def match(command, settings):
for pattern in patterns:
if re.search(pattern, command.stderr):
return True
return False
def get_new_command(command, settings):
for pattern in patterns:
file = re.findall(pattern, command.stderr)
if file:
file = file[0]
dir = file[0:file.rfind('/')]
formatme = shells.and_('mkdir -p {}', '{}')
return formatme.format(dir, command.script)

24
thefuck/rules/open.py Normal file
View File

@ -0,0 +1,24 @@
# Opens URL's in the default web browser
#
# Example:
# > open github.com
# The file ~/github.com does not exist.
# Perhaps you meant 'http://github.com'?
#
#
def match(command, settings):
return (command.script.startswith ('open')
and (
# Wanted to use this:
# 'http' in command.stderr
'.com' in command.script
or '.net' in command.script
or '.org' in command.script
or '.ly' in command.script
or '.io' in command.script
or '.se' in command.script
or '.edu' in command.script))
def get_new_command(command, settings):
return 'open http://' + command.script[5:]

View File

@ -1,23 +1,13 @@
import subprocess import subprocess
from thefuck.utils import DEVNULL from thefuck.utils import DEVNULL, which
from thefuck import shells
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): def __get_pkgfile(command):
try: try:
return subprocess.check_output( return subprocess.check_output(
['pkgfile', '-b', '-v', command.script.split(" ")[0]], ['pkgfile', '-b', '-v', command.script.split(" ")[0]],
universal_newlines=True, stderr=subprocess.DEVNULL universal_newlines=True, stderr=DEVNULL
).split() ).split()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return None return None
@ -30,14 +20,15 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
package = __get_pkgfile(command)[0] package = __get_pkgfile(command)[0]
return '{} -S {} && {}'.format(pacman, package, command.script) formatme = shells.and_('{} -S {}', '{}')
return formatme.format(pacman, package, command.script)
if not __command_available('pkgfile'): if not which('pkgfile'):
enabled_by_default = False enabled_by_default = False
elif __command_available('yaourt'): elif which('yaourt'):
pacman = 'yaourt' pacman = 'yaourt'
elif __command_available('pacman'): elif which('pacman'):
pacman = 'sudo pacman' pacman = 'sudo pacman'
else: else:
enabled_by_default = False enabled_by_default = False

31
thefuck/rules/whois.py Normal file
View File

@ -0,0 +1,31 @@
# -*- encoding: utf-8 -*-
from six.moves.urllib.parse import urlparse
def match(command, settings):
"""
What the `whois` command returns depends on the 'Whois server' it contacted
and is not consistent through different servers. But there can be only two
types of errors I can think of with `whois`:
- `whois https://en.wikipedia.org/` `whois en.wikipedia.org`;
- `whois en.wikipedia.org` `whois wikipedia.org`.
So we match any `whois` command and then:
- if there is a slash: keep only the FQDN;
- if there is no slash but there is a point: removes the left-most
subdomain.
We cannot either remove all subdomains because we cannot know which part is
the subdomains and which is the domain, consider:
- www.google.fr subdomain: www, domain: 'google.fr';
- google.co.uk subdomain: None, domain; 'google.co.uk'.
"""
return 'whois' in command.script and len(command.script.split()) > 1
def get_new_command(command, settings):
url = command.script.split()[1]
if '/' in command.script:
return 'whois ' + urlparse(url).netloc
elif '.' in command.script:
return 'whois ' + '.'.join(urlparse(url).path.split('.')[1:])

View File

@ -1,5 +1,5 @@
"""Module with shell specific actions, each shell class should """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. methods.
""" """
@ -8,15 +8,16 @@ from subprocess import Popen, PIPE
from time import time from time import time
import os import os
from psutil import Process from psutil import Process
from .utils import DEVNULL from .utils import DEVNULL, memoize
class Generic(object): class Generic(object):
def _get_aliases(self):
def get_aliases(self):
return {} return {}
def _expand_aliases(self, command_script): def _expand_aliases(self, command_script):
aliases = self._get_aliases() aliases = self.get_aliases()
binary = command_script.split(' ')[0] binary = command_script.split(' ')[0]
if binary in aliases: if binary in aliases:
return command_script.replace(binary, aliases[binary], 1) return command_script.replace(binary, aliases[binary], 1)
@ -47,16 +48,24 @@ class Generic(object):
with open(history_file_name, 'a') as history: with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script)) history.write(self._get_history_line(command_script))
def and_(self, *commands):
return ' && '.join(commands)
class Bash(Generic): class Bash(Generic):
def app_alias(self):
return "\nalias fuck='eval $(thefuck $(fc -ln -1)); history -r'\n"
def _parse_alias(self, alias): def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1) name, value = alias.replace('alias ', '', 1).split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1] value = value[1:-1]
return name, value return name, value
def _get_aliases(self): @memoize
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
@ -70,15 +79,52 @@ class Bash(Generic):
return u'{}\n'.format(command_script) return u'{}\n'.format(command_script)
class Fish(Generic):
def app_alias(self):
return ("function fuck -d 'Correct your previous console command'\n"
" set -l exit_code $status\n"
" set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_commandd $history[1]\n"
" thefuck $fucked_up_commandd > $eval_script\n"
" . $eval_script\n"
" rm $eval_script\n"
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_commandd\n"
" end\n"
"end")
@memoize
def get_aliases(self):
proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL,
shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {function: function for function in functions}
def _get_history_file_name(self):
return os.path.expanduser('~/.config/fish/fish_history')
def _get_history_line(self, command_script):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def and_(self, *commands):
return '; and '.join(commands)
class Zsh(Generic): class Zsh(Generic):
def app_alias(self):
return "\nalias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'\n"
def _parse_alias(self, alias): def _parse_alias(self, alias):
name, value = alias.split('=', 1) name, value = alias.split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1] value = value[1:-1]
return name, value return name, value
def _get_aliases(self): @memoize
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
@ -92,13 +138,44 @@ class Zsh(Generic):
return u': {}:0;{}\n'.format(int(time()), command_script) return u': {}:0;{}\n'.format(int(time()), command_script)
class Tcsh(Generic):
def app_alias(self):
return "\nalias fuck 'set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`'\n"
def _parse_alias(self, alias):
name, value = alias.split("\t", 1)
return name, value
@memoize
def get_aliases(self):
proc = Popen('tcsh -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 '\t' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.history'))
def _get_history_line(self, command_script):
return u'#+{}\n{}\n'.format(int(time()), command_script)
shells = defaultdict(lambda: Generic(), { shells = defaultdict(lambda: Generic(), {
'bash': Bash(), 'bash': Bash(),
'zsh': Zsh()}) 'fish': Fish(),
'zsh': Zsh(),
'csh': Tcsh(),
'tcsh': Tcsh()})
def _get_shell(): def _get_shell():
shell = Process(os.getpid()).parent().cmdline()[0] try:
shell = Process(os.getpid()).parent().name()
except TypeError:
shell = Process(os.getpid()).parent.name
return shells[shell] return shells[shell]
@ -111,8 +188,16 @@ def to_shell(command):
def app_alias(): def app_alias():
return _get_shell().app_alias() print(_get_shell().app_alias())
def put_to_history(command): def put_to_history(command):
return _get_shell().put_to_history(command) return _get_shell().put_to_history(command)
def and_(*commands):
return _get_shell().and_(*commands)
def get_aliases():
return list(_get_shell().get_aliases().keys())

View File

@ -1,5 +1,6 @@
from functools import wraps from functools import wraps
import os import os
import pickle
import six import six
from .types import Command from .types import Command
@ -62,3 +63,19 @@ def sudo_support(fn):
else: else:
return result return result
return wrapper return wrapper
def memoize(fn):
"""Caches previous calls to the function."""
memo = {}
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo:
memo[key] = fn(*args, **kwargs)
return memo[key]
return wrapper