mirror of
https://github.com/nvbn/thefuck.git
synced 2025-11-01 15:42:06 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db12211e05 | ||
|
|
7a0db1899c | ||
|
|
e5255c3278 | ||
|
|
d44b11fbd8 | ||
|
|
3472026d5e | ||
|
|
bf3c16816d | ||
|
|
6fac0622e5 | ||
|
|
1b694fae7b | ||
|
|
2ebfb92760 | ||
|
|
9cb04ac631 | ||
|
|
5504b905f3 | ||
|
|
e707728fd5 | ||
|
|
3d98aad5df | ||
|
|
b72ad2907f | ||
|
|
7a57355e7e | ||
|
|
1132015e60 | ||
|
|
0ecc86eda6 | ||
|
|
c4848d1816 | ||
|
|
31becc9456 | ||
|
|
cd3a3cd823 | ||
|
|
f9b30ae2d3 | ||
|
|
832ef96188 | ||
|
|
20e678a38a | ||
|
|
f76d2061d1 | ||
|
|
16ec6a7d2a | ||
|
|
6c4333944f | ||
|
|
31f5185642 | ||
|
|
d71dbc5de4 | ||
|
|
fabef80056 | ||
|
|
b4c4fdf706 | ||
|
|
d267488520 | ||
|
|
e31124335f | ||
|
|
71a5182b9a | ||
|
|
6a096155dc | ||
|
|
5742d2d910 | ||
|
|
754bb3e21f | ||
|
|
2bbba9a0c8 | ||
|
|
b978c3793e | ||
|
|
8a83b30e73 | ||
|
|
fd20a3f832 | ||
|
|
b6ed499103 | ||
|
|
76600cf40a | ||
|
|
e62666181a | ||
|
|
c88b0792b8 | ||
|
|
06a89427e2 | ||
|
|
3a134f250d | ||
|
|
b54cdf7c49 | ||
|
|
1b05a497e8 | ||
|
|
79602383ec | ||
|
|
84c42168df | ||
|
|
f53d772ac3 | ||
|
|
93d4a4fc3a | ||
|
|
2cb23b1805 | ||
|
|
33f28cf76d | ||
|
|
6322dbd9ed |
@@ -1 +1,2 @@
|
|||||||
include LICENSE.md
|
include LICENSE.md
|
||||||
|
include fastentrypoints.py
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -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:
|
||||||
@@ -106,13 +108,13 @@ On Ubuntu you can install `The Fuck` with:
|
|||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install python3-dev python3-pip
|
sudo apt install python3-dev python3-pip
|
||||||
pip3 install --user thefuck
|
sudo pip3 install thefuck
|
||||||
```
|
```
|
||||||
|
|
||||||
On other systems you can install `The Fuck` with `pip`:
|
On other systems you can install `The Fuck` with `pip`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install --user thefuck
|
pip install thefuck
|
||||||
```
|
```
|
||||||
|
|
||||||
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
|
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
|
||||||
@@ -130,16 +132,22 @@ 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
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install --user thefuck --upgrade
|
pip install thefuck --upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
**Aliases changed in 1.34.**
|
**Aliases changed in 1.34.**
|
||||||
@@ -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:
|
||||||
@@ -429,12 +454,13 @@ Project License can be found [here](LICENSE.md).
|
|||||||
|
|
||||||
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
|
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
|
||||||
[version-link]: https://pypi.python.org/pypi/thefuck/
|
[version-link]: https://pypi.python.org/pypi/thefuck/
|
||||||
[travis-badge]: https://img.shields.io/travis/nvbn/thefuck.svg
|
[travis-badge]: https://travis-ci.org/nvbn/thefuck.svg?branch=master
|
||||||
[travis-link]: https://travis-ci.org/nvbn/thefuck
|
[travis-link]: https://travis-ci.org/nvbn/thefuck
|
||||||
[appveyor-badge]: https://img.shields.io/appveyor/ci/nvbn/thefuck.svg?label=windows%20build
|
[appveyor-badge]: https://ci.appveyor.com/api/projects/status/1sskj4imj02um0gu/branch/master?svg=true
|
||||||
[appveyor-link]: https://ci.appveyor.com/project/nvbn/thefuck
|
[appveyor-link]: https://ci.appveyor.com/project/nvbn/thefuck
|
||||||
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
|
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
|
||||||
[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
BIN
example_instant_mode.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 535 KiB |
110
fastentrypoints.py
Normal file
110
fastentrypoints.py
Normal 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__)
|
||||||
7
setup.py
7
setup.py
@@ -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.16'
|
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',
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,7 +8,7 @@ shells.shell = shells.Generic()
|
|||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
"""Adds `--run-without-docker` argument."""
|
"""Adds `--enable-functional` argument."""
|
||||||
group = parser.getgroup("thefuck")
|
group = parser.getgroup("thefuck")
|
||||||
group.addoption('--enable-functional', action="store_true", default=False,
|
group.addoption('--enable-functional', action="store_true", default=False,
|
||||||
help="Enable functional tests")
|
help="Enable functional tests")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ parametrize_extensions = pytest.mark.parametrize('ext', tar_extensions)
|
|||||||
# (filename as typed by the user, unquoted filename, quoted filename as per shells.quote)
|
# (filename as typed by the user, unquoted filename, quoted filename as per shells.quote)
|
||||||
parametrize_filename = pytest.mark.parametrize('filename, unquoted, quoted', [
|
parametrize_filename = pytest.mark.parametrize('filename, unquoted, quoted', [
|
||||||
('foo{}', 'foo{}', 'foo{}'),
|
('foo{}', 'foo{}', 'foo{}'),
|
||||||
('foo\ bar{}', 'foo bar{}', "'foo bar{}'"),
|
|
||||||
('"foo bar{}"', 'foo bar{}', "'foo bar{}'")])
|
('"foo bar{}"', 'foo bar{}', "'foo bar{}'")])
|
||||||
|
|
||||||
parametrize_script = pytest.mark.parametrize('script, fixed', [
|
parametrize_script = pytest.mark.parametrize('script, fixed', [
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ def test_side_effect(zip_error, script, filename):
|
|||||||
@pytest.mark.parametrize('script,fixed,filename', [
|
@pytest.mark.parametrize('script,fixed,filename', [
|
||||||
(u'unzip café', u"unzip café -d 'café'", u'café.zip'),
|
(u'unzip café', u"unzip café -d 'café'", u'café.zip'),
|
||||||
(u'unzip foo', u'unzip foo -d foo', u'foo.zip'),
|
(u'unzip foo', u'unzip foo -d foo', u'foo.zip'),
|
||||||
(u"unzip foo\\ bar.zip", u"unzip foo\\ bar.zip -d 'foo bar'", u'foo.zip'),
|
|
||||||
(u"unzip 'foo bar.zip'", u"unzip 'foo bar.zip' -d 'foo bar'", u'foo.zip'),
|
(u"unzip 'foo bar.zip'", u"unzip 'foo bar.zip' -d 'foo bar'", u'foo.zip'),
|
||||||
(u'unzip foo.zip', u'unzip foo.zip -d foo', u'foo.zip')])
|
(u'unzip foo.zip', u'unzip foo.zip -d foo', u'foo.zip')])
|
||||||
def test_get_new_command(zip_error, script, fixed, filename):
|
def test_get_new_command(zip_error, script, fixed, filename):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from tests.utils import Command
|
|||||||
def git_not_command():
|
def git_not_command():
|
||||||
return """git: 'brnch' is not a git command. See 'git --help'.
|
return """git: 'brnch' is not a git command. See 'git --help'.
|
||||||
|
|
||||||
Did you mean this?
|
The most similar command is
|
||||||
branch
|
branch
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ branch
|
|||||||
def git_not_command_one_of_this():
|
def git_not_command_one_of_this():
|
||||||
return """git: 'st' is not a git command. See 'git --help'.
|
return """git: 'st' is not a git command. See 'git --help'.
|
||||||
|
|
||||||
Did you mean one of these?
|
The most similar commands are
|
||||||
status
|
status
|
||||||
reset
|
reset
|
||||||
stage
|
stage
|
||||||
@@ -29,7 +29,7 @@ stats
|
|||||||
def git_not_command_closest():
|
def git_not_command_closest():
|
||||||
return '''git: 'tags' is not a git command. See 'git --help'.
|
return '''git: 'tags' is not a git command. See 'git --help'.
|
||||||
|
|
||||||
Did you mean one of these?
|
The most similar commands are
|
||||||
\tstage
|
\tstage
|
||||||
\ttag
|
\ttag
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_match(script):
|
|||||||
assert match(Command(script))
|
assert match(Command(script))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('script', ['git branch' 'vimfile'])
|
@pytest.mark.parametrize('script', ['git branch', 'vimfile'])
|
||||||
def test_not_match(script):
|
def test_not_match(script):
|
||||||
assert not match(Command(script))
|
assert not match(Command(script))
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ def test_not_match(command):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('command, result', [
|
@pytest.mark.parametrize('command, result', [
|
||||||
(Command('yarn whyy webpack', stderr=stderr('whyy')), 'yarn why webpack')])
|
(Command('yarn whyy webpack', stderr=stderr('whyy')),
|
||||||
|
'yarn why webpack'),
|
||||||
|
(Command('yarn require lodash', stderr=stderr('require')),
|
||||||
|
'yarn add lodash')])
|
||||||
def test_get_new_command(command, result):
|
def test_get_new_command(command, result):
|
||||||
assert get_new_command(command)[0] == result
|
fixed_command = get_new_command(command)
|
||||||
|
if isinstance(fixed_command, list):
|
||||||
|
fixed_command = fixed_command[0]
|
||||||
|
|
||||||
|
assert fixed_command == result
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)')
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(':')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -125,5 +136,5 @@ def configured_successfully(configuration_details):
|
|||||||
|
|
||||||
def version(thefuck_version, python_version):
|
def version(thefuck_version, python_version):
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
u'The Fuck {} using Python {}'.format(thefuck_version,
|
u'The Fuck {} using Python {}\n'.format(thefuck_version,
|
||||||
python_version))
|
python_version))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
18
thefuck/output_readers/__init__.py
Normal file
18
thefuck/output_readers/__init__.py
Normal 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)
|
||||||
82
thefuck/output_readers/read_log.py
Normal file
82
thefuck/output_readers/read_log.py
Normal 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
|
||||||
57
thefuck/output_readers/rerun.py
Normal file
57
thefuck/output_readers/rerun.py
Normal 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
|
||||||
@@ -6,12 +6,13 @@ from thefuck.specific.git import git_support
|
|||||||
@git_support
|
@git_support
|
||||||
def match(command):
|
def match(command):
|
||||||
return (" is not a git command. See 'git --help'." in command.stderr
|
return (" is not a git command. See 'git --help'." in command.stderr
|
||||||
and 'Did you mean' in command.stderr)
|
and ('The most similar command' in command.stderr
|
||||||
|
or 'Did you mean' in command.stderr))
|
||||||
|
|
||||||
|
|
||||||
@git_support
|
@git_support
|
||||||
def get_new_command(command):
|
def get_new_command(command):
|
||||||
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
|
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
|
||||||
command.stderr)[0]
|
command.stderr)[0]
|
||||||
matched = get_all_matched_commands(command.stderr)
|
matched = get_all_matched_commands(command.stderr, ['The most similar command', 'Did you mean'])
|
||||||
return replace_command(command, broken_cmd, matched)
|
return replace_command(command, broken_cmd, matched)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from thefuck.utils import get_all_executables, memoize, which
|
from thefuck.utils import get_all_executables, memoize
|
||||||
|
|
||||||
|
|
||||||
@memoize
|
@memoize
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ patterns = ['permission denied',
|
|||||||
'edspermissionerror',
|
'edspermissionerror',
|
||||||
'you don\'t have write permissions',
|
'you don\'t have write permissions',
|
||||||
'use `sudo`',
|
'use `sudo`',
|
||||||
'SudoRequiredError']
|
'SudoRequiredError',
|
||||||
|
'error: insufficient privileges']
|
||||||
|
|
||||||
|
|
||||||
def match(command):
|
def match(command):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
from thefuck.utils import for_app, eager, replace_command
|
from thefuck.utils import for_app, eager, replace_command, replace_argument
|
||||||
|
|
||||||
regex = re.compile(r'error Command "(.*)" not found.')
|
regex = re.compile(r'error Command "(.*)" not found.')
|
||||||
|
|
||||||
@@ -10,6 +10,9 @@ def match(command):
|
|||||||
return regex.findall(command.stderr)
|
return regex.findall(command.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
npm_commands = {'require': 'add'}
|
||||||
|
|
||||||
|
|
||||||
@eager
|
@eager
|
||||||
def _get_all_tasks():
|
def _get_all_tasks():
|
||||||
proc = Popen(['yarn', '--help'], stdout=PIPE)
|
proc = Popen(['yarn', '--help'], stdout=PIPE)
|
||||||
@@ -27,5 +30,9 @@ def _get_all_tasks():
|
|||||||
|
|
||||||
def get_new_command(command):
|
def get_new_command(command):
|
||||||
misspelled_task = regex.findall(command.stderr)[0]
|
misspelled_task = regex.findall(command.stderr)[0]
|
||||||
tasks = _get_all_tasks()
|
if misspelled_task in npm_commands:
|
||||||
return replace_command(command, misspelled_task, tasks)
|
yarn_command = npm_commands[misspelled_task]
|
||||||
|
return replace_argument(command.script, misspelled_task, yarn_command)
|
||||||
|
else:
|
||||||
|
tasks = _get_all_tasks()
|
||||||
|
return replace_command(command, misspelled_task, tasks)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -11,12 +12,14 @@ class Bash(Generic):
|
|||||||
return '''
|
return '''
|
||||||
function {name} () {{
|
function {name} () {{
|
||||||
TF_PREVIOUS=$(fc -ln -1);
|
TF_PREVIOUS=$(fc -ln -1);
|
||||||
|
TF_PYTHONIOENCODING=$PYTHONIOENCODING;
|
||||||
|
export TF_ALIAS={name};
|
||||||
|
export TF_SHELL_ALIASES=$(alias);
|
||||||
|
export PYTHONIOENCODING=utf-8;
|
||||||
TF_CMD=$(
|
TF_CMD=$(
|
||||||
TF_ALIAS={name}
|
|
||||||
TF_SHELL_ALIASES=$(alias)
|
|
||||||
PYTHONIOENCODING=utf-8
|
|
||||||
thefuck $TF_PREVIOUS {argument_placeholder} $@
|
thefuck $TF_PREVIOUS {argument_placeholder} $@
|
||||||
) && eval $TF_CMD;
|
) && eval $TF_CMD;
|
||||||
|
export PYTHONIOENCODING=$TF_PYTHONIOENCODING;
|
||||||
{alter_history}
|
{alter_history}
|
||||||
}}
|
}}
|
||||||
'''.format(
|
'''.format(
|
||||||
@@ -25,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] == "'":
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 ''
|
||||||
@@ -77,7 +82,7 @@ class Generic(object):
|
|||||||
encoded = self.encode_utf8(command)
|
encoded = self.encode_utf8(command)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
splitted = shlex.split(encoded)
|
splitted = [s.replace("??", "\ ") for s in shlex.split(encoded.replace('\ ', '??'))]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
splitted = encoded.split(' ')
|
splitted = encoded.split(' ')
|
||||||
|
|
||||||
|
|||||||
@@ -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' \
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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] == "'":
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sys
|
|||||||
from .conf import settings
|
from .conf import settings
|
||||||
from .exceptions import NoRuleMatched
|
from .exceptions import NoRuleMatched
|
||||||
from .system import get_key
|
from .system import get_key
|
||||||
|
from .utils import get_alias
|
||||||
from . import logs, const
|
from . import logs, const
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +70,8 @@ def select_command(corrected_commands):
|
|||||||
try:
|
try:
|
||||||
selector = CommandSelector(corrected_commands)
|
selector = CommandSelector(corrected_commands)
|
||||||
except NoRuleMatched:
|
except NoRuleMatched:
|
||||||
logs.failed('No fucks given')
|
logs.failed('No fucks given' if get_alias() == 'fuck'
|
||||||
|
else 'Nothing found')
|
||||||
return
|
return
|
||||||
|
|
||||||
if not settings.require_confirmation:
|
if not settings.require_confirmation:
|
||||||
|
|||||||
@@ -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(':')
|
||||||
@@ -141,12 +138,17 @@ def eager(fn, *args, **kwargs):
|
|||||||
|
|
||||||
@eager
|
@eager
|
||||||
def get_all_matched_commands(stderr, separator='Did you mean'):
|
def get_all_matched_commands(stderr, separator='Did you mean'):
|
||||||
|
if not isinstance(separator, list):
|
||||||
|
separator = [separator]
|
||||||
should_yield = False
|
should_yield = False
|
||||||
for line in stderr.split('\n'):
|
for line in stderr.split('\n'):
|
||||||
if separator in line:
|
for sep in separator:
|
||||||
should_yield = True
|
if sep in line:
|
||||||
elif should_yield and line:
|
should_yield = True
|
||||||
yield line.strip()
|
break
|
||||||
|
else:
|
||||||
|
if should_yield and line:
|
||||||
|
yield line.strip()
|
||||||
|
|
||||||
|
|
||||||
def replace_command(command, broken, matched):
|
def replace_command(command, broken, matched):
|
||||||
@@ -250,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]
|
||||||
|
|
||||||
|
|
||||||
@@ -278,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()
|
||||||
|
|||||||
Reference in New Issue
Block a user