1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 15:42:06 +00:00

Compare commits

...

36 Commits
3.19 ... 3.23

Author SHA1 Message Date
Vladimir Iakovlev
db12211e05 Bump to 3.23 2017-08-29 09:39:32 +02:00
Vladimir Iakovlev
7a0db1899c #685: Warn about Python 2 only on Python 2 2017-08-29 09:39:24 +02:00
Vladimir Iakovlev
e5255c3278 Bump to 3.22 2017-08-29 05:02:16 +02:00
Vladimir Iakovlev
d44b11fbd8 #682: Fix gif link 2017-08-28 03:39:17 +02:00
Vladimir Iakovlev
3472026d5e #685: Warn about Python 2 support 2017-08-28 03:38:14 +02:00
Vladimir Iakovlev
bf3c16816d Merge pull request #684 from nvbn/682-instant-fuck-mode
682 instant fuck mode
2017-08-28 04:35:51 +03:00
Vladimir Iakovlev
6fac0622e5 #682: Warn on instant mode with Python 2 2017-08-28 03:21:15 +02:00
Vladimir Iakovlev
1b694fae7b #682: Fix gif link 2017-08-26 14:41:05 +02:00
Vladimir Iakovlev
2ebfb92760 #682: Add gif with instant mode 2017-08-26 14:39:36 +02:00
Vladimir Iakovlev
9cb04ac631 #682: Make warnings more visible 2017-08-26 14:30:19 +02:00
Vladimir Iakovlev
5504b905f3 #682: Fix git_push rule in instant mode 2017-08-26 13:39:38 +02:00
Vladimir Iakovlev
e707728fd5 #682: Update readme 2017-08-26 13:31:09 +02:00
Vladimir Iakovlev
3d98aad5df Merge branch 'master' into 682-instant-fuck-mode 2017-08-26 13:25:59 +02:00
Vladimir Iakovlev
b72ad2907f #682: Allow THEFUCK_INSTANT_MODE=False 2017-08-26 13:21:24 +02:00
Vladimir Iakovlev
7a57355e7e #682: Disable instant mode on Python 2 2017-08-26 13:16:10 +02:00
Vladimir Iakovlev
1132015e60 #682: Rename output to output_readers 2017-08-26 12:45:49 +02:00
Vladimir Iakovlev
0ecc86eda6 #682: Fix aliases in instant mode 2017-08-26 06:29:38 +02:00
Vladimir Iakovlev
c4848d1816 #682: Fix tests in python 2 2017-08-26 06:20:52 +02:00
Vladimir Iakovlev
31becc9456 #682: Fix tests and flake8 2017-08-26 06:16:51 +02:00
Vladimir Iakovlev
cd3a3cd823 #682: Implement instant mode aliases for bash and zsh 2017-08-26 05:46:07 +02:00
Vladimir Iakovlev
f9b30ae2d3 #683: Mention -y and -r in the readme 2017-08-26 04:57:16 +02:00
Vladimir Iakovlev
832ef96188 #681: Lower priority of missing_space_before_subcommand rule 2017-08-25 11:47:17 +02:00
Vladimir Iakovlev
20e678a38a #682: Implement experimental instant mode 2017-08-25 11:44:07 +02:00
Vladimir Iakovlev
f76d2061d1 Merge pull request #680 from simonwhitaker/patch-1
Fix docs for Command type
2017-08-23 09:37:13 +03:00
Simon Whitaker
16ec6a7d2a Fix docs for Command type 2017-08-23 07:14:56 +01:00
Vladimir Iakovlev
6c4333944f Bump to 3.21 2017-08-21 12:26:19 +02:00
Vladimir Iakovlev
31f5185642 Merge pull request #679 from nvbn/678-speedup-thefuck-alias
678 speedup thefuck
2017-08-21 13:25:33 +03:00
Vladimir Iakovlev
d71dbc5de4 #678: Speedup fuck by hardcoding entry points 2017-08-21 11:55:34 +02:00
Vladimir Iakovlev
fabef80056 #678: Import pkg_resources only when it needed 2017-08-21 11:50:04 +02:00
Vladimir Iakovlev
b4c4fdf706 #678: Use fastentrypoints 2017-08-21 11:32:23 +02:00
Vladimir Iakovlev
d267488520 Bump to 3.20 2017-08-16 11:28:59 +02:00
Vladimir Iakovlev
e31124335f #658: Ensure that history isn't empty in autoconfiguration 2017-08-16 11:26:43 +02:00
Vladimir Iakovlev
71a5182b9a Merge pull request #676 from nvbn/662-fix-autoconfig
#662: Autoconfigure when `fuck` was called < 60 seconds ago from the same shell
2017-08-08 17:36:10 +02:00
Vladimir Iakovlev
6a096155dc #662: Autoconfigure when fuck was called < 60 seconds ago from the same shell 2017-08-08 16:13:37 +02:00
Vladimir Iakovlev
5742d2d910 #N/A: Use real PATH in tests 2017-08-03 12:30:04 +02:00
Vladimir Iakovlev
754bb3e21f #N/A: Reset environment variables in tests 2017-08-03 12:18:05 +02:00
35 changed files with 547 additions and 167 deletions

View File

@@ -1 +1,2 @@
include LICENSE.md include LICENSE.md
include fastentrypoints.py

View File

@@ -4,6 +4,8 @@ Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/) inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320). [tweet](https://twitter.com/liamosaur/status/506975850596536320).
The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode)
[![gif with examples][examples-link]][examples-link] [![gif with examples][examples-link]][examples-link]
Few more examples: Few more examples:
@@ -130,10 +132,16 @@ eval $(thefuck --alias FUCK)
Changes will be available only in a new shell session. Changes will be available only in a new shell session.
To make them available immediately, run `source ~/.bashrc` (or your shell config file like `.zshrc`). To make them available immediately, run `source ~/.bashrc` (or your shell config file like `.zshrc`).
If you want separate alias for running fixed command without confirmation you can use alias like: If you want to run fixed command without confirmation you can use `-y` option:
```bash ```bash
alias fuck-it='export THEFUCK_REQUIRE_CONFIRMATION=False; fuck; export THEFUCK_REQUIRE_CONFIRMATION=True' fuck -y
```
If you want to fix commands recursively until success you can use `-r` option:
```bash
fuck -r
``` ```
## Update ## Update
@@ -298,7 +306,7 @@ side_effect(old_command: Command, fixed_command: str) -> None
``` ```
and optional `enabled_by_default`, `requires_output` and `priority` variables. and optional `enabled_by_default`, `requires_output` and `priority` variables.
`Command` has three attributes: `script`, `stdout`, `stderr` and `script_parts`. `Command` has four attributes: `script`, `stdout`, `stderr` and `script_parts`.
Rule shouldn't change `Command`. Rule shouldn't change `Command`.
@@ -389,6 +397,23 @@ export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000' export THEFUCK_HISTORY_LIMIT='2000'
``` ```
## Experimental instant mode
By default The Fuck reruns a previous command and that takes time,
in instant mode The Fuck logs output with [script](https://en.wikipedia.org/wiki/Script_(Unix))
and just reads the log.
[![gif with instant mode][instant-mode-gif-link]][instant-mode-gif-link]
At the moment only Python 3 with bash or zsh is supported.
For enabling instant mode you need to add `--enable-experimental-instant-mode`
to alias initialization in your `.bashrc`, `.bash_profile` or `.zshrc` like:
```bash
eval $(thefuck --alias --enable-experimental-instant-mode)
```
## Developing ## Developing
Install `The Fuck` for development: Install `The Fuck` for development:
@@ -437,4 +462,5 @@ Project License can be found [here](LICENSE.md).
[coverage-link]: https://coveralls.io/github/nvbn/thefuck [coverage-link]: https://coveralls.io/github/nvbn/thefuck
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg [license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
[examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif [examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif
[instant-mode-gif-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example_instant_mode.gif
[homebrew]: http://brew.sh/ [homebrew]: http://brew.sh/

BIN
example_instant_mode.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

110
fastentrypoints.py Normal file
View File

@@ -0,0 +1,110 @@
# Copyright (c) 2016, Aaron Christianson
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
Monkey patch setuptools to write faster console_scripts with this format:
import sys
from mymodule import entry_function
sys.exit(entry_function())
This is better.
(c) 2016, Aaron Christianson
http://github.com/ninjaaron/fast-entry_points
'''
from setuptools.command import easy_install
import re
TEMPLATE = '''\
# -*- coding: utf-8 -*-
# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}'
__requires__ = '{3}'
import re
import sys
from {0} import {1}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit({2}())'''
@classmethod
def get_args(cls, dist, header=None):
"""
Yield write_script() argument tuples for a distribution's
console_scripts and gui_scripts entry points.
"""
if header is None:
header = cls.get_header()
spec = str(dist.as_requirement())
for type_ in 'console', 'gui':
group = type_ + '_scripts'
for name, ep in dist.get_entry_map(group).items():
# ensure_safe_name
if re.search(r'[\\/]', name):
raise ValueError("Path separators not allowed in script names")
script_text = TEMPLATE.format(
ep.module_name, ep.attrs[0], '.'.join(ep.attrs),
spec, group, name)
args = cls._get_script_args(type_, name, header, script_text)
for res in args:
yield res
easy_install.ScriptWriter.get_args = get_args
def main():
import os
import re
import shutil
import sys
dests = sys.argv[1:] or ['.']
filename = re.sub('\.pyc$', '.py', __file__)
for dst in dests:
shutil.copy(filename, dst)
manifest_path = os.path.join(dst, 'MANIFEST.in')
setup_path = os.path.join(dst, 'setup.py')
# Insert the include statement to MANIFEST.in if not present
with open(manifest_path, 'a+') as manifest:
manifest.seek(0)
manifest_content = manifest.read()
if not 'include fastentrypoints.py' in manifest_content:
manifest.write(('\n' if manifest_content else '')
+ 'include fastentrypoints.py')
# Insert the import statement to setup.py if not present
with open(setup_path, 'a+') as setup:
setup.seek(0)
setup_content = setup.read()
if not 'import fastentrypoints' in setup_content:
setup.seek(0)
setup.truncate()
setup.write('import fastentrypoints\n' + setup_content)
print(__name__)

View File

@@ -3,6 +3,8 @@ from setuptools import setup, find_packages
import pkg_resources import pkg_resources
import sys import sys
import os import os
import fastentrypoints
try: try:
if int(pkg_resources.get_distribution("pip").version.split('.')[0]) < 6: if int(pkg_resources.get_distribution("pip").version.split('.')[0]) < 6:
@@ -29,10 +31,11 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version)) ' ({}.{} detected).'.format(*version))
sys.exit(-1) sys.exit(-1)
VERSION = '3.19' VERSION = '3.23'
install_requires = ['psutil', 'colorama', 'six', 'decorator'] install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte']
extras_require = {':python_version<"3.4"': ['pathlib2'], extras_require = {':python_version<"3.4"': ['pathlib2'],
':python_version<"3.3"': ['backports.shutil_get_terminal_size'],
":sys_platform=='win32'": ['win_unicode_console']} ":sys_platform=='win32'": ['win_unicode_console']}
setup(name='thefuck', setup(name='thefuck',

View File

@@ -1,3 +1,4 @@
import os
import pytest import pytest
from thefuck import shells from thefuck import shells
from thefuck import conf, const from thefuck import conf, const
@@ -56,7 +57,13 @@ def set_shell(monkeypatch, request):
def _set(cls): def _set(cls):
shell = cls() shell = cls()
monkeypatch.setattr('thefuck.shells.shell', shell) monkeypatch.setattr('thefuck.shells.shell', shell)
request.addfinalizer()
return shell return shell
return _set return _set
@pytest.fixture(autouse=True)
def os_environ(monkeypatch):
env = {'PATH': os.environ['PATH']}
monkeypatch.setattr('os.environ', env)
return env

View File

@@ -50,8 +50,8 @@ def test_match(command):
assert match(command) assert match(command)
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, url', [
(Command('yarn help clean', stdout=stdout_clean), (Command('yarn help clean', stdout=stdout_clean),
open_command('https://yarnpkg.com/en/docs/cli/clean'))]) 'https://yarnpkg.com/en/docs/cli/clean')])
def test_get_new_command(command, new_command): def test_get_new_command(command, url):
assert get_new_command(command) == new_command assert get_new_command(command) == open_command(url)

View File

@@ -18,17 +18,14 @@ class TestFish(object):
b'man\nmath\npopd\npushd\nruby') b'man\nmath\npopd\npushd\nruby')
return mock return mock
@pytest.fixture
def os_environ(self, monkeypatch, key, value):
monkeypatch.setattr('os.environ', {key: value})
@pytest.mark.parametrize('key, value', [ @pytest.mark.parametrize('key, value', [
('TF_OVERRIDDEN_ALIASES', 'cut,git,sed'), # legacy ('TF_OVERRIDDEN_ALIASES', 'cut,git,sed'), # legacy
('THEFUCK_OVERRIDDEN_ALIASES', 'cut,git,sed'), ('THEFUCK_OVERRIDDEN_ALIASES', 'cut,git,sed'),
('THEFUCK_OVERRIDDEN_ALIASES', 'cut, git, sed'), ('THEFUCK_OVERRIDDEN_ALIASES', 'cut, git, sed'),
('THEFUCK_OVERRIDDEN_ALIASES', ' cut,\tgit,sed\n'), ('THEFUCK_OVERRIDDEN_ALIASES', ' cut,\tgit,sed\n'),
('THEFUCK_OVERRIDDEN_ALIASES', '\ncut,\n\ngit,\tsed\r')]) ('THEFUCK_OVERRIDDEN_ALIASES', '\ncut,\n\ngit,\tsed\r')])
def test_get_overridden_aliases(self, shell, os_environ): def test_get_overridden_aliases(self, shell, os_environ, key, value):
os_environ[key] = value
assert shell._get_overridden_aliases() == {'cd', 'cut', 'git', 'grep', assert shell._get_overridden_aliases() == {'cd', 'cut', 'git', 'grep',
'ls', 'man', 'open', 'sed'} 'ls', 'man', 'open', 'sed'}

View File

@@ -6,7 +6,8 @@ from thefuck.const import ARGUMENT_PLACEHOLDER
def _args(**override): def _args(**override):
args = {'alias': None, 'command': [], 'yes': False, args = {'alias': None, 'command': [], 'yes': False,
'help': False, 'version': False, 'debug': False, 'help': False, 'version': False, 'debug': False,
'force_command': None, 'repeat': False} 'force_command': None, 'repeat': False,
'enable_experimental_instant_mode': False}
args.update(override) args.update(override)
return args return args
@@ -14,6 +15,8 @@ def _args(**override):
@pytest.mark.parametrize('argv, result', [ @pytest.mark.parametrize('argv, result', [
(['thefuck'], _args()), (['thefuck'], _args()),
(['thefuck', '-a'], _args(alias='fuck')), (['thefuck', '-a'], _args(alias='fuck')),
(['thefuck', '--alias', '--enable-experimental-instant-mode'],
_args(alias='fuck', enable_experimental_instant_mode=True)),
(['thefuck', '-a', 'fix'], _args(alias='fix')), (['thefuck', '-a', 'fix'], _args(alias='fix')),
(['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'], (['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'],
_args(command=['git', 'branch'], yes=True)), _args(command=['git', 'branch'], yes=True)),

View File

@@ -10,14 +10,6 @@ def load_source(mocker):
return mocker.patch('thefuck.conf.load_source') 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, settings): def test_settings_defaults(load_source, settings):
load_source.return_value = object() load_source.return_value = object()
settings.init() settings.init()
@@ -25,7 +17,6 @@ def test_settings_defaults(load_source, settings):
assert getattr(settings, key) == val assert getattr(settings, key) == val
@pytest.mark.usefixture('environ')
class TestSettingsFromFile(object): class TestSettingsFromFile(object):
def test_from_file(self, load_source, settings): def test_from_file(self, load_source, settings):
load_source.return_value = Mock(rules=['test'], load_source.return_value = Mock(rules=['test'],
@@ -54,15 +45,15 @@ class TestSettingsFromFile(object):
@pytest.mark.usefixture('load_source') @pytest.mark.usefixture('load_source')
class TestSettingsFromEnv(object): class TestSettingsFromEnv(object):
def test_from_env(self, environ, settings): def test_from_env(self, os_environ, settings):
environ.update({'THEFUCK_RULES': 'bash:lisp', os_environ.update({'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_EXCLUDE_RULES': 'git:vim', 'THEFUCK_EXCLUDE_RULES': 'git:vim',
'THEFUCK_WAIT_COMMAND': '55', 'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true', 'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false', 'THEFUCK_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15', 'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
'THEFUCK_WAIT_SLOW_COMMAND': '999', 'THEFUCK_WAIT_SLOW_COMMAND': '999',
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew'}) 'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew'})
settings.init() settings.init()
assert settings.rules == ['bash', 'lisp'] assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim'] assert settings.exclude_rules == ['git', 'vim']
@@ -73,8 +64,8 @@ class TestSettingsFromEnv(object):
assert settings.wait_slow_command == 999 assert settings.wait_slow_command == 999
assert settings.slow_commands == ['lein', 'react-native', './gradlew'] assert settings.slow_commands == ['lein', 'react-native', './gradlew']
def test_from_env_with_DEFAULT(self, environ, settings): def test_from_env_with_DEFAULT(self, os_environ, settings):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
settings.init() settings.init()
assert settings.rules == const.DEFAULT_RULES + ['bash', 'lisp'] assert settings.rules == const.DEFAULT_RULES + ['bash', 'lisp']
@@ -116,15 +107,15 @@ class TestInitializeSettingsFile(object):
(False, '/user/test/config/', '/user/test/config/thefuck'), (False, '/user/test/config/', '/user/test/config/thefuck'),
(True, '~/.config', '~/.thefuck'), (True, '~/.config', '~/.thefuck'),
(True, '/user/test/config/', '~/.thefuck')]) (True, '/user/test/config/', '~/.thefuck')])
def test_get_user_dir_path(mocker, environ, settings, legacy_dir_exists, def test_get_user_dir_path(mocker, os_environ, settings, legacy_dir_exists,
xdg_config_home, result): xdg_config_home, result):
mocker.patch('thefuck.conf.Path.is_dir', mocker.patch('thefuck.conf.Path.is_dir',
return_value=legacy_dir_exists) return_value=legacy_dir_exists)
if xdg_config_home is not None: if xdg_config_home is not None:
environ['XDG_CONFIG_HOME'] = xdg_config_home os_environ['XDG_CONFIG_HOME'] = xdg_config_home
else: else:
environ.pop('XDG_CONFIG_HOME', None) os_environ.pop('XDG_CONFIG_HOME', None)
path = settings._get_user_dir_path().as_posix() path = settings._get_user_dir_path().as_posix()
assert path == os.path.expanduser(result) assert path == os.path.expanduser(result)

View File

@@ -1,4 +1,6 @@
import pytest import pytest
import json
from six import StringIO
from mock import MagicMock from mock import MagicMock
from thefuck.shells.generic import ShellConfiguration from thefuck.shells.generic import ShellConfiguration
from thefuck.not_configured import main from thefuck.not_configured import main
@@ -11,19 +13,33 @@ def usage_tracker(mocker):
new_callable=MagicMock) new_callable=MagicMock)
def _assert_tracker_updated(usage_tracker, pid): @pytest.fixture(autouse=True)
def usage_tracker_io(usage_tracker):
io = StringIO()
usage_tracker.return_value \ usage_tracker.return_value \
.open.return_value \ .open.return_value \
.__enter__.return_value \ .__enter__.return_value = io
.write.assert_called_once_with(str(pid)) return io
def _change_tracker(usage_tracker, pid): @pytest.fixture(autouse=True)
usage_tracker.return_value.exists.return_value = True def usage_tracker_exists(usage_tracker):
usage_tracker.return_value \ usage_tracker.return_value \
.open.return_value \ .exists.return_value = True
.__enter__.return_value \ return usage_tracker.return_value.exists
.read.return_value = str(pid)
def _assert_tracker_updated(usage_tracker_io, pid):
usage_tracker_io.seek(0)
info = json.load(usage_tracker_io)
assert info['pid'] == pid
def _change_tracker(usage_tracker_io, pid):
usage_tracker_io.truncate(0)
info = {'pid': pid, 'time': 0}
json.dump(info, usage_tracker_io)
usage_tracker_io.seek(0)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -67,29 +83,28 @@ def test_for_generic_shell(shell, logs):
logs.how_to_configure_alias.assert_called_once() logs.how_to_configure_alias.assert_called_once()
def test_on_first_run(usage_tracker, shell_pid, logs): def test_on_first_run(usage_tracker_io, usage_tracker_exists, shell_pid, logs):
shell_pid.return_value = 12 shell_pid.return_value = 12
usage_tracker.return_value.exists.return_value = False
main() main()
_assert_tracker_updated(usage_tracker, 12) usage_tracker_exists.return_value = False
_assert_tracker_updated(usage_tracker_io, 12)
logs.how_to_configure_alias.assert_called_once() logs.how_to_configure_alias.assert_called_once()
def test_on_run_after_other_commands(usage_tracker, shell_pid, shell, logs): def test_on_run_after_other_commands(usage_tracker_io, shell_pid, shell, logs):
shell_pid.return_value = 12 shell_pid.return_value = 12
shell.get_history.return_value = ['fuck', 'ls'] shell.get_history.return_value = ['fuck', 'ls']
_change_tracker(usage_tracker, 12) _change_tracker(usage_tracker_io, 12)
main() main()
logs.how_to_configure_alias.assert_called_once() logs.how_to_configure_alias.assert_called_once()
def test_on_first_run_from_current_shell(usage_tracker, shell_pid, def test_on_first_run_from_current_shell(usage_tracker_io, shell_pid,
shell, logs): shell, logs):
shell.get_history.return_value = ['fuck'] shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12 shell_pid.return_value = 12
_change_tracker(usage_tracker, 55)
main() main()
_assert_tracker_updated(usage_tracker, 12) _assert_tracker_updated(usage_tracker_io, 12)
logs.how_to_configure_alias.assert_called_once() logs.how_to_configure_alias.assert_called_once()
@@ -104,21 +119,21 @@ def test_when_cant_configure_automatically(shell_pid, shell, logs):
logs.how_to_configure_alias.assert_called_once() logs.how_to_configure_alias.assert_called_once()
def test_when_already_configured(usage_tracker, shell_pid, def test_when_already_configured(usage_tracker_io, shell_pid,
shell, shell_config, logs): shell, shell_config, logs):
shell.get_history.return_value = ['fuck'] shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12 shell_pid.return_value = 12
_change_tracker(usage_tracker, 12) _change_tracker(usage_tracker_io, 12)
shell_config.read.return_value = 'eval $(thefuck --alias)' shell_config.read.return_value = 'eval $(thefuck --alias)'
main() main()
logs.already_configured.assert_called_once() logs.already_configured.assert_called_once()
def test_when_successfuly_configured(usage_tracker, shell_pid, def test_when_successfully_configured(usage_tracker_io, shell_pid,
shell, shell_config, logs): shell, shell_config, logs):
shell.get_history.return_value = ['fuck'] shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12 shell_pid.return_value = 12
_change_tracker(usage_tracker, 12) _change_tracker(usage_tracker_io, 12)
shell_config.read.return_value = '' shell_config.read.return_value = ''
main() main()
shell_config.write.assert_any_call('eval $(thefuck --alias)') shell_config.write.assert_any_call('eval $(thefuck --alias)')

View File

@@ -110,16 +110,15 @@ class TestCommand(object):
Popen = Mock() Popen = Mock()
Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr' Popen.return_value.stderr.read.return_value = b'stderr'
monkeypatch.setattr('thefuck.types.Popen', Popen) monkeypatch.setattr('thefuck.output_readers.rerun.Popen', Popen)
return Popen return Popen
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def prepare(self, monkeypatch): def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.types.os.environ', {}) monkeypatch.setattr('thefuck.output_readers.rerun._wait_output',
monkeypatch.setattr('thefuck.types.Command._wait_output', lambda *_: True)
staticmethod(lambda *_: True))
def test_from_script_calls(self, Popen, settings): def test_from_script_calls(self, Popen, settings, os_environ):
settings.env = {} settings.env = {}
assert Command.from_raw_script( assert Command.from_raw_script(
['apt-get', 'search', 'vim']) == Command( ['apt-get', 'search', 'vim']) == Command(
@@ -129,7 +128,7 @@ class TestCommand(object):
stdin=PIPE, stdin=PIPE,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env={}) env=os_environ)
@pytest.mark.parametrize('script, result', [ @pytest.mark.parametrize('script, result', [
([''], None), ([''], None),

View File

@@ -69,34 +69,40 @@ class TestSelectCommand(object):
def test_without_confirmation(self, capsys, commands, settings): def test_without_confirmation(self, capsys, commands, settings):
settings.require_confirmation = False settings.require_confirmation = False
assert ui.select_command(iter(commands)) == commands[0] assert ui.select_command(iter(commands)) == commands[0]
assert capsys.readouterr() == ('', 'ls\n') assert capsys.readouterr() == ('', const.USER_COMMAND_MARK + 'ls\n')
def test_without_confirmation_with_side_effects( def test_without_confirmation_with_side_effects(
self, capsys, commands_with_side_effect, settings): self, capsys, commands_with_side_effect, settings):
settings.require_confirmation = False settings.require_confirmation = False
assert (ui.select_command(iter(commands_with_side_effect)) assert (ui.select_command(iter(commands_with_side_effect))
== commands_with_side_effect[0]) == commands_with_side_effect[0])
assert capsys.readouterr() == ('', 'ls (+side effect)\n') assert capsys.readouterr() == ('', const.USER_COMMAND_MARK + 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_get_key, commands): def test_with_confirmation(self, capsys, patch_get_key, commands):
patch_get_key(['\n']) patch_get_key(['\n'])
assert ui.select_command(iter(commands)) == commands[0] assert ui.select_command(iter(commands)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') assert capsys.readouterr() == (
'', const.USER_COMMAND_MARK + u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_get_key, commands): def test_with_confirmation_abort(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_CTRL_C]) patch_get_key([const.KEY_CTRL_C])
assert ui.select_command(iter(commands)) is None assert ui.select_command(iter(commands)) is None
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') assert capsys.readouterr() == (
'', const.USER_COMMAND_MARK + u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n')
def test_with_confirmation_with_side_effct(self, capsys, patch_get_key, def test_with_confirmation_with_side_effct(self, capsys, patch_get_key,
commands_with_side_effect): commands_with_side_effect):
patch_get_key(['\n']) patch_get_key(['\n'])
assert (ui.select_command(iter(commands_with_side_effect)) assert (ui.select_command(iter(commands_with_side_effect))
== commands_with_side_effect[0]) == commands_with_side_effect[0])
assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') assert capsys.readouterr() == (
'', const.USER_COMMAND_MARK + u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_select_second(self, capsys, patch_get_key, commands): def test_with_confirmation_select_second(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_DOWN, '\n']) patch_get_key([const.KEY_DOWN, '\n'])
assert ui.select_command(iter(commands)) == commands[1] assert ui.select_command(iter(commands)) == commands[1]
assert capsys.readouterr() == ( stderr = (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') u'{mark}\x1b[1K\rls [enter/↑/↓/ctrl+c]'
u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n'
).format(mark=const.USER_COMMAND_MARK)
assert capsys.readouterr() == ('', stderr)

View File

@@ -206,8 +206,7 @@ class TestGetValidHistoryWithoutCurrent(object):
return_value='fuck') return_value='fuck')
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def bins(self, mocker, monkeypatch): def bins(self, mocker):
monkeypatch.setattr('thefuck.conf.os.environ', {'PATH': 'path'})
callables = list() callables = list()
for name in ['diff', 'ls', 'café']: for name in ['diff', 'ls', 'café']:
bin_mock = mocker.Mock(name=name) bin_mock = mocker.Mock(name=name)

View File

@@ -25,6 +25,10 @@ class Parser(object):
nargs='?', nargs='?',
const=get_alias(), const=get_alias(),
help='[custom-alias-name] prints alias for current shell') help='[custom-alias-name] prints alias for current shell')
self._parser.add_argument(
'--enable-experimental-instant-mode',
action='store_true',
help='enable experimental instant mode, use on your own risk')
self._parser.add_argument( self._parser.add_argument(
'-h', '--help', '-h', '--help',
action='store_true', action='store_true',

View File

@@ -98,7 +98,7 @@ class Settings(dict):
elif attr in ('wait_command', 'history_limit', 'wait_slow_command'): elif attr in ('wait_command', 'history_limit', 'wait_slow_command'):
return int(val) return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug', elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history'): 'alter_history', 'instant_mode'):
return val.lower() == 'true' return val.lower() == 'true'
elif attr == 'slow_commands': elif attr == 'slow_commands':
return val.split(':') return val.split(':')

View File

@@ -35,6 +35,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'slow_commands': ['lein', 'react-native', 'gradle', 'slow_commands': ['lein', 'react-native', 'gradle',
'./gradlew', 'vagrant'], './gradlew', 'vagrant'],
'repeat': False, 'repeat': False,
'instant_mode': False,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}} 'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@@ -48,7 +49,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_ALTER_HISTORY': 'alter_history', 'THEFUCK_ALTER_HISTORY': 'alter_history',
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command', 'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands', 'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat'} 'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode'}
SETTINGS_HEADER = u"""# The Fuck settings file SETTINGS_HEADER = u"""# The Fuck settings file
# #
@@ -63,3 +65,9 @@ SETTINGS_HEADER = u"""# The Fuck settings file
""" """
ARGUMENT_PLACEHOLDER = 'THEFUCK_ARGUMENT_PLACEHOLDER' ARGUMENT_PLACEHOLDER = 'THEFUCK_ARGUMENT_PLACEHOLDER'
CONFIGURATION_TIMEOUT = 60
USER_COMMAND_MARK = u'\u200B' * 10
LOG_SIZE = 1000

View File

@@ -4,3 +4,7 @@ class EmptyCommand(Exception):
class NoRuleMatched(Exception): class NoRuleMatched(Exception):
"""Raised when no rule matched for some command.""" """Raised when no rule matched for some command."""
class ScriptNotInLog(Exception):
"""Script not found in log."""

View File

@@ -6,6 +6,7 @@ import sys
from traceback import format_exception from traceback import format_exception
import colorama import colorama
from .conf import settings from .conf import settings
from . import const
def color(color_): def color(color_):
@@ -16,6 +17,14 @@ def color(color_):
return color_ return color_
def warn(title):
sys.stderr.write(u'{warn}[WARN] {title}{reset}\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL),
title=title))
def exception(title, exc_info): def exception(title, exc_info):
sys.stderr.write( sys.stderr.write(
u'{warn}[WARN] {title}:{reset}\n{trace}' u'{warn}[WARN] {title}:{reset}\n{trace}'
@@ -39,7 +48,8 @@ def failed(msg):
def show_corrected_command(corrected_command): def show_corrected_command(corrected_command):
sys.stderr.write(u'{bold}{script}{reset}{side_effect}\n'.format( sys.stderr.write(u'{prefix}{bold}{script}{reset}{side_effect}\n'.format(
prefix=const.USER_COMMAND_MARK,
script=corrected_command.script, script=corrected_command.script,
side_effect=u' (+side effect)' if corrected_command.side_effect else u'', side_effect=u' (+side effect)' if corrected_command.side_effect else u'',
bold=color(colorama.Style.BRIGHT), bold=color(colorama.Style.BRIGHT),
@@ -48,9 +58,10 @@ def show_corrected_command(corrected_command):
def confirm_text(corrected_command): def confirm_text(corrected_command):
sys.stderr.write( sys.stderr.write(
(u'{clear}{bold}{script}{reset}{side_effect} ' (u'{prefix}{clear}{bold}{script}{reset}{side_effect} '
u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}' u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
u'/{red}ctrl+c{reset}]').format( u'/{red}ctrl+c{reset}]').format(
prefix=const.USER_COMMAND_MARK,
script=corrected_command.script, script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '', side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r', clear='\033[1K\r',

View File

@@ -5,6 +5,7 @@ init_output()
from pprint import pformat # noqa: E402 from pprint import pformat # noqa: E402
import sys # noqa: E402 import sys # noqa: E402
import six # noqa: E402
from . import logs, types # noqa: E402 from . import logs, types # noqa: E402
from .shells import shell # noqa: E402 from .shells import shell # noqa: E402
from .conf import settings # noqa: E402 from .conf import settings # noqa: E402
@@ -13,6 +14,7 @@ from .exceptions import EmptyCommand # noqa: E402
from .ui import select_command # noqa: E402 from .ui import select_command # noqa: E402
from .argument_parser import Parser # noqa: E402 from .argument_parser import Parser # noqa: E402
from .utils import get_installation_info # noqa: E402 from .utils import get_installation_info # noqa: E402
from .logs import warn # noqa: E402
def fix_command(known_args): def fix_command(known_args):
@@ -50,6 +52,19 @@ def main():
elif known_args.command: elif known_args.command:
fix_command(known_args) fix_command(known_args)
elif known_args.alias: elif known_args.alias:
print(shell.app_alias(known_args.alias)) if six.PY2:
warn("The Fuck will drop Python 2 support soon, more details "
"https://github.com/nvbn/thefuck/issues/685")
if known_args.enable_experimental_instant_mode:
if six.PY2:
warn("Instant mode not supported with Python 2")
alias = shell.app_alias(known_args.alias)
else:
alias = shell.instant_mode_alias(known_args.alias)
else:
alias = shell.app_alias(known_args.alias)
print(alias)
else: else:
parser.print_usage() parser.print_usage()

View File

@@ -4,9 +4,11 @@ from .system import init_output
init_output() init_output()
import os # noqa: E402 import os # noqa: E402
from psutil import Process # noqa: E402 import json # noqa: E402
import time # noqa: E402
import six # noqa: E402 import six # noqa: E402
from . import logs # noqa: E402 from psutil import Process # noqa: E402
from . import logs, const # noqa: E402
from .shells import shell # noqa: E402 from .shells import shell # noqa: E402
from .conf import settings # noqa: E402 from .conf import settings # noqa: E402
from .system import Path # noqa: E402 from .system import Path # noqa: E402
@@ -30,19 +32,41 @@ def _get_not_configured_usage_tracker_path():
def _record_first_run(): def _record_first_run():
"""Records shell pid to tracker file.""" """Records shell pid to tracker file."""
with _get_not_configured_usage_tracker_path().open('w') as tracker: info = {'pid': _get_shell_pid(),
tracker.write(six.text_type(_get_shell_pid())) 'time': time.time()}
mode = 'wb' if six.PY2 else 'w'
with _get_not_configured_usage_tracker_path().open(mode) as tracker:
json.dump(info, tracker)
def _get_previous_command():
history = shell.get_history()
if history:
return history[-1]
else:
return None
def _is_second_run(): def _is_second_run():
"""Returns `True` when we know that `fuck` called second time.""" """Returns `True` when we know that `fuck` called second time."""
tracker_path = _get_not_configured_usage_tracker_path() tracker_path = _get_not_configured_usage_tracker_path()
if not tracker_path.exists() or not shell.get_history()[-1] == 'fuck': if not tracker_path.exists():
return False return False
current_pid = _get_shell_pid() current_pid = _get_shell_pid()
with tracker_path.open('r') as tracker: with tracker_path.open('r') as tracker:
return tracker.read() == six.text_type(current_pid) try:
info = json.load(tracker)
except ValueError:
return False
if not (isinstance(info, dict) and info.get('pid') == current_pid):
return False
return (_get_previous_command() == 'fuck' or
time.time() - info.get('time', 0) < const.CONFIGURATION_TIMEOUT)
def _is_already_configured(configuration_details): def _is_already_configured(configuration_details):

View File

@@ -0,0 +1,18 @@
from ..conf import settings
from . import read_log, rerun
def get_output(script, expanded):
"""Get output of the script.
:param script: Console script.
:type script: str
:param expanded: Console script with expanded aliases.
:type expanded: str
:rtype: (str, str)
"""
if settings.instant_mode:
return read_log.get_output(script)
else:
return rerun.get_output(script, expanded)

View File

@@ -0,0 +1,82 @@
import os
import shlex
try:
from shutil import get_terminal_size
except ImportError:
from backports.shutil_get_terminal_size import get_terminal_size
import six
import pyte
from ..exceptions import ScriptNotInLog
from ..logs import warn
from .. import const
def _group_by_calls(log):
script_line = None
lines = []
for line in log:
try:
line = line.decode()
except UnicodeDecodeError:
continue
if const.USER_COMMAND_MARK in line:
if script_line:
yield script_line, lines
script_line = line
lines = [line]
elif script_line is not None:
lines.append(line)
if script_line:
yield script_line, lines
def _get_script_group_lines(grouped, script):
parts = shlex.split(script)
for script_line, lines in reversed(grouped):
if all(part in script_line for part in parts):
return lines
raise ScriptNotInLog
def _get_output_lines(script, log_file):
lines = log_file.readlines()[-const.LOG_SIZE:]
grouped = list(_group_by_calls(lines))
script_lines = _get_script_group_lines(grouped, script)
screen = pyte.Screen(get_terminal_size().columns, len(script_lines))
stream = pyte.Stream(screen)
stream.feed(''.join(script_lines))
return screen.display
def get_output(script):
"""Reads script output from log.
:type script: str
:rtype: (str, str)
"""
if six.PY2:
warn('Experimental instant mode is Python 3+ only')
return None, None
if 'THEFUCK_OUTPUT_LOG' not in os.environ:
warn("Output log isn't specified")
return None, None
try:
with open(os.environ['THEFUCK_OUTPUT_LOG'], 'rb') as log_file:
lines = _get_output_lines(script, log_file)
output = '\n'.join(lines).strip()
return output, output
except OSError:
warn("Can't read output log")
return None, None
except ScriptNotInLog:
warn("Script not found in output log")
return None, None

View File

@@ -0,0 +1,57 @@
import os
import shlex
from subprocess import Popen, PIPE
from psutil import Process, TimeoutExpired
from .. import logs
from ..conf import settings
def _wait_output(popen, is_slow):
"""Returns `True` if we can get output of the command in the
`settings.wait_command` time.
Command will be killed if it wasn't finished in the time.
:type popen: Popen
:rtype: bool
"""
proc = Process(popen.pid)
try:
proc.wait(settings.wait_slow_command if is_slow
else settings.wait_command)
return True
except TimeoutExpired:
for child in proc.children(recursive=True):
child.kill()
proc.kill()
return False
def get_output(script, expanded):
"""Runs the script and obtains stdin/stderr.
:type script: str
:type expanded: str
:rtype: (str, str)
"""
env = dict(os.environ)
env.update(settings.env)
is_slow = shlex.split(expanded) in settings.slow_commands
with logs.debug_time(u'Call: {}; with env: {}; is slow: '.format(
script, env, is_slow)):
result = Popen(expanded, shell=True, stdin=PIPE,
stdout=PIPE, stderr=PIPE, env=env)
if _wait_output(result, is_slow):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
logs.debug(u'Received stdout: {}'.format(stdout))
logs.debug(u'Received stderr: {}'.format(stderr))
return stdout, stderr
else:
logs.debug(u'Execution timed out!')
return None, None

View File

@@ -1,3 +1,4 @@
import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support from thefuck.specific.git import git_support
@@ -32,5 +33,6 @@ def get_new_command(command):
if len(command_parts) > upstream_option_index: if len(command_parts) > upstream_option_index:
command_parts.pop(upstream_option_index) command_parts.pop(upstream_option_index)
push_upstream = command.stderr.split('\n')[-3].strip().partition('git ')[2] arguments = re.findall(r'git push (.*)', command.stderr)[0].strip()
return replace_argument(" ".join(command_parts), 'push', push_upstream) return replace_argument(" ".join(command_parts), 'push',
'push {}'.format(arguments))

View File

@@ -16,3 +16,6 @@ def match(command):
def get_new_command(command): def get_new_command(command):
executable = _get_executable(command.script_parts[0]) executable = _get_executable(command.script_parts[0])
return command.script.replace(executable, u'{} '.format(executable), 1) return command.script.replace(executable, u'{} '.format(executable), 1)
priority = 4000

View File

@@ -1,6 +1,7 @@
import os import os
from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import memoize
from .generic import Generic from .generic import Generic
@@ -27,6 +28,21 @@ class Bash(Generic):
alter_history=('history -s $TF_CMD;' alter_history=('history -s $TF_CMD;'
if settings.alter_history else '')) if settings.alter_history else ''))
def instant_mode_alias(self, alias_name):
if os.environ.get('THEFUCK_INSTANT_MODE', '').lower() == 'true':
return '''
export PS1="{user_command_mark}$PS1";
{app_alias}
'''.format(user_command_mark=USER_COMMAND_MARK,
app_alias=self.app_alias(alias_name))
else:
return '''
export THEFUCK_INSTANT_MODE=True;
export THEFUCK_OUTPUT_LOG={log};
script -feq {log};
exit
'''.format(log='/tmp/thefuck-script-log-{}'.format(uuid4().hex))
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] == "'":

View File

@@ -18,7 +18,7 @@ class Fish(Generic):
default.add(alias.strip()) default.add(alias.strip())
return default return default
def app_alias(self, fuck): def app_alias(self, alias_name):
if settings.alter_history: if settings.alter_history:
alter_history = (' builtin history delete --exact' alter_history = (' builtin history delete --exact'
' --case-sensitive -- $fucked_up_command\n' ' --case-sensitive -- $fucked_up_command\n'
@@ -33,7 +33,7 @@ class Fish(Generic):
' if [ "$unfucked_command" != "" ]\n' ' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n{1}' ' eval $unfucked_command\n{1}'
' end\n' ' end\n'
'end').format(fuck, alter_history) 'end').format(alias_name, alter_history)
@memoize @memoize
@cache('.config/fish/config.fish', '.config/fish/functions') @cache('.config/fish/config.fish', '.config/fish/functions')

View File

@@ -3,6 +3,7 @@ import os
import shlex import shlex
import six import six
from collections import namedtuple from collections import namedtuple
from ..logs import warn
from ..utils import memoize from ..utils import memoize
from ..conf import settings from ..conf import settings
from ..system import Path from ..system import Path
@@ -32,9 +33,13 @@ class Generic(object):
"""Prepares command for running in shell.""" """Prepares command for running in shell."""
return command_script return command_script
def app_alias(self, fuck): def app_alias(self, alias_name):
return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \ return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"thefuck $(fc -ln -1))'".format(fuck) "thefuck $(fc -ln -1))'".format(alias_name)
def instant_mode_alias(self, alias_name):
warn("Instant mode not supported by your shell")
return self.app_alias(alias_name)
def _get_history_file_name(self): def _get_history_file_name(self):
return '' return ''

View File

@@ -2,8 +2,8 @@ from .generic import Generic, ShellConfiguration
class Powershell(Generic): class Powershell(Generic):
def app_alias(self, fuck): def app_alias(self, alias_name):
return 'function ' + fuck + ' {\n' \ return 'function ' + alias_name + ' {\n' \
' $history = (Get-History -Count 1).CommandLine;\n' \ ' $history = (Get-History -Count 1).CommandLine;\n' \
' if (-not [string]::IsNullOrWhiteSpace($history)) {\n' \ ' if (-not [string]::IsNullOrWhiteSpace($history)) {\n' \
' $fuck = $(thefuck $history);\n' \ ' $fuck = $(thefuck $history);\n' \

View File

@@ -6,10 +6,10 @@ from .generic import Generic
class Tcsh(Generic): class Tcsh(Generic):
def app_alias(self, fuck): def app_alias(self, alias_name):
return ("alias {0} 'setenv TF_ALIAS {0} && " return ("alias {0} 'setenv TF_ALIAS {0} && "
"set fucked_cmd=`history -h 2 | head -n 1` && " "set fucked_cmd=`history -h 2 | head -n 1` && "
"eval `thefuck ${{fucked_cmd}}`'").format(fuck) "eval `thefuck ${{fucked_cmd}}`'").format(alias_name)
def _parse_alias(self, alias): def _parse_alias(self, alias):
name, value = alias.split("\t", 1) name, value = alias.split("\t", 1)

View File

@@ -1,7 +1,8 @@
from time import time from time import time
import os import os
from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import memoize
from .generic import Generic from .generic import Generic
@@ -26,6 +27,21 @@ class Zsh(Generic):
alter_history=('test -n "$TF_CMD" && print -s $TF_CMD' alter_history=('test -n "$TF_CMD" && print -s $TF_CMD'
if settings.alter_history else '')) if settings.alter_history else ''))
def instant_mode_alias(self, alias_name):
if os.environ.get('THEFUCK_INSTANT_MODE', '').lower() == 'true':
return '''
export PS1="{user_command_mark}$PS1";
{app_alias}
'''.format(user_command_mark=USER_COMMAND_MARK,
app_alias=self.app_alias(alias_name))
else:
return '''
export THEFUCK_INSTANT_MODE=True;
export THEFUCK_OUTPUT_LOG={log};
script -feq {log};
exit
'''.format(log='/tmp/thefuck-script-log-{}'.format(uuid4().hex))
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] == "'":

View File

@@ -1,15 +1,13 @@
from imp import load_source from imp import load_source
from subprocess import Popen, PIPE
import os import os
import sys import sys
import six
from psutil import Process, TimeoutExpired
from . import logs from . import logs
from .shells import shell from .shells import shell
from .conf import settings from .conf import settings
from .const import DEFAULT_PRIORITY, ALL_ENABLED from .const import DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand from .exceptions import EmptyCommand
from .utils import get_alias from .utils import get_alias, format_raw_script
from .output_readers import get_output
class Command(object): class Command(object):
@@ -61,44 +59,6 @@ class Command(object):
kwargs.setdefault('stderr', self.stderr) kwargs.setdefault('stderr', self.stderr)
return Command(**kwargs) return Command(**kwargs)
@staticmethod
def _wait_output(popen, is_slow):
"""Returns `True` if we can get output of the command in the
`settings.wait_command` time.
Command will be killed if it wasn't finished in the time.
:type popen: Popen
:rtype: bool
"""
proc = Process(popen.pid)
try:
proc.wait(settings.wait_slow_command if is_slow
else settings.wait_command)
return True
except TimeoutExpired:
for child in proc.children(recursive=True):
child.kill()
proc.kill()
return False
@staticmethod
def _prepare_script(raw_script):
"""Creates single script from a list of script parts.
:type raw_script: [basestring]
:rtype: basestring
"""
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in raw_script)
else:
script = ' '.join(raw_script)
script = script.strip()
return shell.from_shell(script)
@classmethod @classmethod
def from_raw_script(cls, raw_script): def from_raw_script(cls, raw_script):
"""Creates instance of `Command` from a list of script parts. """Creates instance of `Command` from a list of script parts.
@@ -108,29 +68,13 @@ class Command(object):
:raises: EmptyCommand :raises: EmptyCommand
""" """
script = cls._prepare_script(raw_script) script = format_raw_script(raw_script)
if not script: if not script:
raise EmptyCommand raise EmptyCommand
env = dict(os.environ) expanded = shell.from_shell(script)
env.update(settings.env) stdout, stderr = get_output(script, expanded)
return cls(expanded, stdout, stderr)
is_slow = script.split(' ')[0] in settings.slow_commands
with logs.debug_time(u'Call: {}; with env: {}; is slow: '.format(
script, env, is_slow)):
result = Popen(script, shell=True, stdin=PIPE,
stdout=PIPE, stderr=PIPE, env=env)
if cls._wait_output(result, is_slow):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
logs.debug(u'Received stdout: {}'.format(stdout))
logs.debug(u'Received stderr: {}'.format(stderr))
return cls(script, stdout, stderr)
else:
logs.debug(u'Execution timed out!')
return cls(script, None, None)
class Rule(object): class Rule(object):

View File

@@ -1,6 +1,5 @@
import os import os
import pickle import pickle
import pkg_resources
import re import re
import shelve import shelve
import six import six
@@ -8,7 +7,7 @@ from contextlib import closing
from decorator import decorator from decorator import decorator
from difflib import get_close_matches from difflib import get_close_matches
from functools import wraps from functools import wraps
from warnings import warn from .logs import warn
from .conf import settings from .conf import settings
from .system import Path from .system import Path
@@ -108,9 +107,7 @@ def get_all_executables():
return fallback return fallback
tf_alias = get_alias() tf_alias = get_alias()
tf_entry_points = get_installation_info().get_entry_map()\ tf_entry_points = ['thefuck', 'fuck']
.get('console_scripts', {})\
.keys()
bins = [exe.name.decode('utf8') if six.PY2 else exe.name bins = [exe.name.decode('utf8') if six.PY2 else exe.name
for path in os.environ.get('PATH', '').split(':') for path in os.environ.get('PATH', '').split(':')
@@ -255,6 +252,8 @@ cache.disabled = False
def get_installation_info(): def get_installation_info():
import pkg_resources
return pkg_resources.require('thefuck')[0] return pkg_resources.require('thefuck')[0]
@@ -283,3 +282,18 @@ def get_valid_history_without_current(command):
return [line for line in _not_corrected(history, tf_alias) return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script if not line.startswith(tf_alias) and not line == command.script
and line.split(' ')[0] in executables] and line.split(' ')[0] in executables]
def format_raw_script(raw_script):
"""Creates single script from a list of script parts.
:type raw_script: [basestring]
:rtype: basestring
"""
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in raw_script)
else:
script = ' '.join(raw_script)
return script.strip()

View File

@@ -7,4 +7,4 @@ commands = py.test -v --capture=sys
[flake8] [flake8]
ignore = E501,W503 ignore = E501,W503
exclude = venv,build,.tox exclude = venv,build,.tox,setup.py,fastentrypoints.py