1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 07:32:09 +00:00

Compare commits

...

85 Commits
3.15 ... 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
Vladimir Iakovlev
2bbba9a0c8 Bump to 3.19 2017-08-03 11:34:01 +02:00
Vladimir Iakovlev
b978c3793e Merge pull request #669 from tomoshi0809/master
#630 Catching the escaped space in filenames
2017-07-22 20:11:03 +02:00
KEI
8a83b30e73 Corrected the part for splitting a command 2017-07-19 00:09:21 +09:00
Vladimir Iakovlev
fd20a3f832 Merge pull request #657 from josephfrazier/git_not_command-wording
Update stderr wording of git_not_command
2017-06-19 23:12:46 +02:00
Joseph Frazier
b6ed499103 Make git_not_command stderr detection backward-compatible 2017-06-06 13:56:13 -04:00
Joseph Frazier
76600cf40a Update stderr wording of git_not_command
This changed in git v2.13.1, see
6c48686263 (diff-081cf476dd9ac3b05c183570de47cb23)
2017-06-05 17:29:42 -04:00
Vladimir Iakovlev
e62666181a #650: #651: #646: Recommend to install thefuck globally 2017-05-29 10:11:15 +02:00
Vladimir Iakovlev
c88b0792b8 Bump to 3.18 2017-05-10 16:51:19 +02:00
Vladimir Iakovlev
06a89427e2 #N/A: Fix bash alias on ci 2017-05-10 16:40:57 +02:00
Vladimir Iakovlev
3a134f250d Bump to 3.17 2017-05-10 15:34:06 +02:00
Vladimir Iakovlev
b54cdf7c49 #637: Suggest yarn add on yarn require 2017-05-10 15:32:11 +02:00
Vladimir Iakovlev
1b05a497e8 #635: Show "Nothing found" instead of 'No fucks given' when different alias are used 2017-05-10 15:22:26 +02:00
Vladimir Iakovlev
79602383ec #549: Fix aliases with bash 2017-05-10 15:14:01 +02:00
Vladimir Iakovlev
84c42168df #N/A: Add new line after version 2017-05-10 15:06:29 +02:00
Vladimir Iakovlev
f53d772ac3 Merge pull request #640 from bam241/add_macport_to_sudo
fix sudo.py for macport
2017-05-03 12:08:53 +04:00
Mouginot B
93d4a4fc3a fix sudo.py for macport 2017-05-02 17:36:35 -05:00
Vladimir Iakovlev
2cb23b1805 #N/A: Fix docstring 2017-05-01 17:49:13 +02:00
Vladimir Iakovlev
33f28cf76d #633: Show ci badges for master 2017-04-20 21:34:47 +02:00
Vladimir Iakovlev
6322dbd9ed #N/A: Fix flake8 warnings 2017-04-10 23:23:23 +02:00
Vladimir Iakovlev
fc09818351 Bump to 3.16 2017-04-10 23:16:06 +02:00
Vladimir Iakovlev
2788ef1471 #N/A: Make missing_space_before_subcommand handle aliases correctly 2017-04-10 23:15:12 +02:00
Vladimir Iakovlev
ef3aabe7c5 Merge branch '614-repeate-option' 2017-03-28 18:51:25 +02:00
Vladimir Iakovlev
2af54d036d #623: Fix UnicodeDecodeError with Python 2 2017-03-28 18:50:51 +02:00
Vladimir Iakovlev
99c10b50ff Merge branch 'dfadev-master' 2017-03-28 18:35:54 +02:00
Vladimir Iakovlev
802fcd96fd #621: Refine yarn_command_replaced rule tests 2017-03-28 18:35:40 +02:00
Russ Panula
900e83e028 add rule for: yarn install [pkg]
--- `install` has been replaced with `add` to add new dependencies. Run $0 instead.

6e9a9a6596/src/reporters/lang/en.js (L18)
2017-03-28 18:31:01 +02:00
Joseph Frazier
d41cbb6810 Fix heroku_not_command for new stderr format
heroku updated its command suggestion formatting, so account for that.
For example:

    $ heroku log
     ▸    log is not a heroku command.
     ▸    Perhaps you meant logs?
     ▸    Run heroku _ to run heroku logs.
     ▸    Run heroku help for a list of available commands.
    $ fuck
    heroku logs [enter/↑/↓/ctrl+c]
2017-03-28 18:31:01 +02:00
Vladimir Iakovlev
b36cf59b46 #614: Refine argument_parser 2017-03-28 18:18:01 +02:00
Vladimir Iakovlev
cfa831c88d #614: Add --repeat option 2017-03-28 18:09:38 +02:00
Vladimir Iakovlev
818d06fb95 Merge pull request #622 from josephfrazier/heroku-format
Fix heroku_not_command for new stderr format
2017-03-28 16:56:03 +04:00
Vladimir Iakovlev
c3eca8234a #620: Add --debug 2017-03-28 13:09:11 +02:00
Vladimir Iakovlev
d47ff8cbf2 #620: Fix functional tests 2017-03-28 12:28:34 +02:00
Vladimir Iakovlev
1a52e98fbd #620: Fix code style 2017-03-28 12:25:33 +02:00
Vladimir Iakovlev
53c11d2ef4 #620: Fix python 2 support 2017-03-28 12:08:32 +02:00
Vladimir Iakovlev
beda1854cf #620: Add bash support 2017-03-28 12:01:09 +02:00
Vladimir Iakovlev
7532c65c62 #620: Fix aliases with zsh 2017-03-28 11:38:28 +02:00
Vladimir Iakovlev
ec37998a10 #620: Add support of arguments to fuck, like fuck -y on zsh 2017-03-28 11:31:06 +02:00
Joseph Frazier
58d5eff6d0 Fix heroku_not_command for new stderr format
heroku updated its command suggestion formatting, so account for that.
For example:

    $ heroku log
     ▸    log is not a heroku command.
     ▸    Perhaps you meant logs?
     ▸    Run heroku _ to run heroku logs.
     ▸    Run heroku help for a list of available commands.
    $ fuck
    heroku logs [enter/↑/↓/ctrl+c]
2017-03-26 15:55:03 -04:00
Vladimir Iakovlev
d28567bb31 #585: Fix suggestion of .bash_profile 2017-03-23 16:55:24 +01:00
Vladimir Iakovlev
b016bb2255 Merge pull request #619 from josephfrazier/yarn-alias-scripts
Extend yarn_alias rule to handle package.json scripts
2017-03-23 19:39:11 +04:00
Joseph Frazier
bf109ee548 Extend yarn_alias rule to handle package.json scripts
For example, if an "etl" script is defined in package.json, it can be
run with `yarn etl`. However, if `yarn etil` is run, `yarn` will
suggest the correction. This change lets `thefuck` take advantage of
that:

    $ yarn etil
    yarn etil v0.21.3
    error Command "etil" not found. Did you mean "etl"?
    $ fuck
    yarn etl [enter/?/?/ctrl+c]
2017-03-22 16:52:30 -04:00
Vladimir Iakovlev
1aaaca1220 Merge branch 'Asday-master' 2017-03-22 14:00:18 +01:00
Vladimir Iakovlev
b096560469 #618: Refine git_push_without_commits rule 2017-03-22 14:00:03 +01:00
Vladimir Iakovlev
5b1f3ff816 Merge branch 'master' of git://github.com/Asday/thefuck into Asday-master 2017-03-22 13:57:18 +01:00
Vladimir Iakovlev
c5f7c89222 Merge pull request #617 from josephfrazier/git-stash-add-updated
git_stash_pop: Add only updated files
2017-03-22 15:25:25 +04:00
Adam Barnes
e61271dae3 Removed another unused import.
Goodness.
2017-03-22 10:59:27 +00:00
Adam Barnes
bddb43b987 Removed an unused import. 2017-03-22 10:29:50 +00:00
Adam Barnes
b22a3ac891 Created a rule for trying to push a new repository with no commits. 2017-03-22 10:23:35 +00:00
Joseph Frazier
f4cc88f6c7 git_stash_pop: Add only updated files
This avoids adding untracked files to the repo. See here for a
description of the difference between `git add .` and `git add --update`:

https://stackoverflow.com/questions/572549/difference-between-git-add-a-and-git-add/572660#572660
2017-03-21 20:12:15 -04:00
61 changed files with 974 additions and 299 deletions

View File

@@ -1 +1,2 @@
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/)
[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]
Few more examples:
@@ -106,13 +108,13 @@ On Ubuntu you can install `The Fuck` with:
```bash
sudo apt update
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`:
```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)
@@ -130,16 +132,22 @@ eval $(thefuck --alias FUCK)
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`).
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
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
```bash
pip install --user thefuck --upgrade
pip install thefuck --upgrade
```
**Aliases changed in 1.34.**
@@ -188,6 +196,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_pull_uncommitted_changes` &ndash; stashes changes before pulling and pops them afterwards;
* `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` &ndash; runs `git pull` when `push` was rejected;
* `git_push_without_commits` &ndash; Creates an initial commit if you forget and only `git add .`, when setting up a new project;
* `git_rebase_no_changes` &ndash; runs `git rebase --skip` instead of `git rebase --continue` when there are no changes;
* `git_rm_local_modifications` &ndash; adds `-f` or `--cached` when you try to `rm` a locally modified file;
* `git_rm_recursive` &ndash; adds `-r` when you try to `rm` a directory;
@@ -258,6 +267,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `workon_doesnt_exists` &ndash; fixes `virtualenvwrapper` env name os suggests to create new.
* `yarn_alias` &ndash; fixes aliased `yarn` commands like `yarn ls`;
* `yarn_command_not_found` &ndash; fixes misspelled `yarn` commands;
* `yarn_command_replaced` &ndash; fixes replaced `yarn` commands;
* `yarn_help` &ndash; makes it easier to open `yarn` documentation;
Enabled by default only on specific platforms:
@@ -296,7 +306,7 @@ side_effect(old_command: Command, fixed_command: str) -> None
```
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`.
@@ -387,6 +397,23 @@ export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
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
Install `The Fuck` for development:
@@ -427,12 +454,13 @@ Project License can be found [here](LICENSE.md).
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
[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
[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
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
[coverage-link]: https://coveralls.io/github/nvbn/thefuck
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
[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/

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

View File

@@ -1,3 +1,4 @@
import os
import pytest
from thefuck import shells
from thefuck import conf, const
@@ -7,7 +8,7 @@ shells.shell = shells.Generic()
def pytest_addoption(parser):
"""Adds `--run-without-docker` argument."""
"""Adds `--enable-functional` argument."""
group = parser.getgroup("thefuck")
group.addoption('--enable-functional', action="store_true", default=False,
help="Enable functional tests")
@@ -56,7 +57,13 @@ def set_shell(monkeypatch, request):
def _set(cls):
shell = cls()
monkeypatch.setattr('thefuck.shells.shell', shell)
request.addfinalizer()
return shell
return _set
@pytest.fixture(autouse=True)
def os_environ(monkeypatch):
env = {'PATH': os.environ['PATH']}
monkeypatch.setattr('os.environ', env)
return env

View File

@@ -81,6 +81,5 @@ def without_confirmation(proc, TIMEOUT):
def how_to_configure(proc, TIMEOUT):
proc.sendline(u'unalias fuck')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u"alias isn't configured"])

View File

@@ -48,4 +48,5 @@ def test_without_confirmation(proc, TIMEOUT):
@pytest.mark.functional
def test_how_to_configure_alias(proc, TIMEOUT):
proc.sendline('unset -f fuck')
how_to_configure(proc, TIMEOUT)

View File

@@ -55,4 +55,5 @@ def test_without_confirmation(proc, TIMEOUT):
@pytest.mark.functional
def test_how_to_configure_alias(proc, TIMEOUT):
proc.sendline(u'unfunction fuck')
how_to_configure(proc, TIMEOUT)

View File

@@ -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)
parametrize_filename = pytest.mark.parametrize('filename, unquoted, quoted', [
('foo{}', 'foo{}', 'foo{}'),
('foo\ bar{}', 'foo bar{}', "'foo bar{}'"),
('"foo bar{}"', 'foo bar{}', "'foo bar{}'")])
parametrize_script = pytest.mark.parametrize('script, fixed', [

View File

@@ -64,7 +64,6 @@ def test_side_effect(zip_error, script, filename):
@pytest.mark.parametrize('script,fixed,filename', [
(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\\ 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')])
def test_get_new_command(zip_error, script, fixed, filename):

View File

@@ -7,7 +7,7 @@ from tests.utils import Command
def git_not_command():
return """git: 'brnch' is not a git command. See 'git --help'.
Did you mean this?
The most similar command is
branch
"""
@@ -16,7 +16,7 @@ branch
def git_not_command_one_of_this():
return """git: 'st' is not a git command. See 'git --help'.
Did you mean one of these?
The most similar commands are
status
reset
stage
@@ -29,7 +29,7 @@ stats
def git_not_command_closest():
return '''git: 'tags' is not a git command. See 'git --help'.
Did you mean one of these?
The most similar commands are
\tstage
\ttag
'''

View File

@@ -0,0 +1,27 @@
import pytest
from tests.utils import Command
from thefuck.rules.git_push_without_commits import (
fix,
get_new_command,
match,
)
command = 'git push -u origin master'
expected_error = '''
error: src refspec master does not match any.
error: failed to push some refs to 'git@github.com:User/repo.git'
'''
@pytest.mark.parametrize('command', [Command(command, stderr=expected_error)])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command, result', [(
Command(command, stderr=expected_error),
fix.format(command=command),
)])
def test_get_new_command(command, result):
assert get_new_command(command) == result

View File

@@ -15,4 +15,4 @@ def test_match(stderr):
def test_get_new_command(stderr):
assert (get_new_command(Command('git stash pop', stderr=stderr))
== "git add . && git stash pop && git reset .")
== "git add --update && git stash pop && git reset .")

View File

@@ -1,34 +1,31 @@
# -*- coding: utf-8 -*-
import pytest
from tests.utils import Command
from thefuck.rules.heroku_not_command import match, get_new_command
def suggest_stderr(cmd):
return ''' ! `{}` is not a heroku command.
! Perhaps you meant `logs`, `pg`.
! See `heroku help` for a list of available commands.'''.format(cmd)
suggest_stderr = '''
log is not a heroku command.
Perhaps you meant logs?
▸ Run heroku _ to run heroku logs.
▸ Run heroku help for a list of available commands.'''
no_suggest_stderr = ''' ! `aaaaa` is not a heroku command.
! See `heroku help` for a list of available commands.'''
@pytest.mark.parametrize('cmd', ['log', 'pge'])
@pytest.mark.parametrize('cmd', ['log'])
def test_match(cmd):
assert match(
Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd)))
Command('heroku {}'.format(cmd), stderr=suggest_stderr))
@pytest.mark.parametrize('script, stderr', [
('cat log', suggest_stderr('log')),
('heroku aaa', no_suggest_stderr)])
('cat log', suggest_stderr)])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr))
@pytest.mark.parametrize('cmd, result', [
('log', ['heroku logs', 'heroku pg']),
('pge', ['heroku pg', 'heroku logs'])])
('log', 'heroku logs')])
def test_get_new_command(cmd, result):
command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd))
command = Command('heroku {}'.format(cmd), stderr=suggest_stderr)
assert get_new_command(command) == result

View File

@@ -4,12 +4,6 @@ from thefuck.rules.missing_space_before_subcommand import (
from tests.utils import Command
@pytest.fixture(autouse=True)
def which(mocker):
return mocker.patch('thefuck.rules.missing_space_before_subcommand.which',
return_value=None)
@pytest.fixture(autouse=True)
def all_executables(mocker):
return mocker.patch(
@@ -23,11 +17,8 @@ def test_match(script):
assert match(Command(script))
@pytest.mark.parametrize('script, which_result', [
('git branch', '/usr/bin/git'),
('vimfile', None)])
def test_not_match(script, which_result, which):
which.return_value = which_result
@pytest.mark.parametrize('script', ['git branch', 'vimfile'])
def test_not_match(script):
assert not match(Command(script))

View File

@@ -4,12 +4,13 @@ from tests.utils import Command
stderr_remove = 'error Did you mean `yarn remove`?'
stderr_etl = 'error Command "etil" not found. Did you mean "etl"?'
stderr_list = 'error Did you mean `yarn list`?'
@pytest.mark.parametrize('command', [
Command(script='yarn rm', stderr=stderr_remove),
Command(script='yarn etil', stderr=stderr_etl),
Command(script='yarn ls', stderr=stderr_list)])
def test_match(command):
assert match(command)
@@ -17,6 +18,7 @@ def test_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('yarn rm', stderr=stderr_remove), 'yarn remove'),
(Command('yarn etil', stderr=stderr_etl), 'yarn etl'),
(Command('yarn ls', stderr=stderr_list), 'yarn list')])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -106,6 +106,13 @@ def test_not_match(command):
@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):
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

View File

@@ -0,0 +1,32 @@
import pytest
from tests.utils import Command
from thefuck.rules.yarn_command_replaced import match, get_new_command
stderr = ('error `install` has been replaced with `add` to add new '
'dependencies. Run "yarn add {}" instead.').format
@pytest.mark.parametrize('command', [
Command(script='yarn install redux', stderr=stderr('redux')),
Command(script='yarn install moment', stderr=stderr('moment')),
Command(script='yarn install lodash', stderr=stderr('lodash'))])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('yarn install')])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('yarn install redux', stderr=stderr('redux')),
'yarn add redux'),
(Command('yarn install moment', stderr=stderr('moment')),
'yarn add moment'),
(Command('yarn install lodash', stderr=stderr('lodash')),
'yarn add lodash')])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

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

View File

@@ -33,6 +33,9 @@ class TestBash(object):
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_or_(self, shell):
assert shell.or_('ls', 'cd') == 'ls || cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',
@@ -40,18 +43,17 @@ class TestBash(object):
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'fuck () {' in shell.app_alias('fuck')
assert 'FUCK () {' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS=fuck' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING' in shell.app_alias('fuck')
def test_app_alias_variables_correctly_set(self, shell):
alias = shell.app_alias('fuck')
assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias
assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias
assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias
assert 'ALIASES=$(alias) thefuck' in alias
assert "fuck () {" in alias
assert "TF_ALIAS=fuck" in alias
assert 'PYTHONIOENCODING=utf-8' in alias
assert 'TF_SHELL_ALIASES=$(alias)' in alias
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])

View File

@@ -18,17 +18,14 @@ class TestFish(object):
b'man\nmath\npopd\npushd\nruby')
return mock
@pytest.fixture
def os_environ(self, monkeypatch, key, value):
monkeypatch.setattr('os.environ', {key: value})
@pytest.mark.parametrize('key, value', [
('TF_OVERRIDDEN_ALIASES', 'cut,git,sed'), # legacy
('THEFUCK_OVERRIDDEN_ALIASES', 'cut,git,sed'),
('THEFUCK_OVERRIDDEN_ALIASES', 'cut, git, sed'),
('THEFUCK_OVERRIDDEN_ALIASES', ' cut,\tgit,sed\n'),
('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',
'ls', 'man', 'open', 'sed'}
@@ -55,6 +52,9 @@ class TestFish(object):
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_or_(self, shell):
assert shell.or_('foo', 'bar') == 'foo; or bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fish_config': 'fish_config',
'fuck': 'fuck',

View File

@@ -18,6 +18,9 @@ class TestGeneric(object):
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_or_(self, shell):
assert shell.or_('ls', 'cd') == 'ls || cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}

View File

@@ -34,6 +34,9 @@ class TestTcsh(object):
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_or_(self, shell):
assert shell.or_('ls', 'cd') == 'ls || cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',

View File

@@ -32,6 +32,9 @@ class TestZsh(object):
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_or_(self, shell):
assert shell.or_('ls', 'cd') == 'ls || cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {
'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))',
@@ -40,17 +43,17 @@ class TestZsh(object):
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'fuck () {' in shell.app_alias('fuck')
assert 'FUCK () {' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING' in shell.app_alias('fuck')
def test_app_alias_variables_correctly_set(self, shell):
alias = shell.app_alias('fuck')
assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias
assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias
assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias
assert 'ALIASES=$(alias) thefuck' in alias
assert "fuck () {" in alias
assert "TF_ALIAS=fuck" in alias
assert 'PYTHONIOENCODING=utf-8' in alias
assert 'TF_SHELL_ALIASES=$(alias)' in alias
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])

View File

@@ -0,0 +1,32 @@
import pytest
from thefuck.argument_parser import Parser
from thefuck.const import ARGUMENT_PLACEHOLDER
def _args(**override):
args = {'alias': None, 'command': [], 'yes': False,
'help': False, 'version': False, 'debug': False,
'force_command': None, 'repeat': False,
'enable_experimental_instant_mode': False}
args.update(override)
return args
@pytest.mark.parametrize('argv, result', [
(['thefuck'], _args()),
(['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', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'],
_args(command=['git', 'branch'], yes=True)),
(['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-y'],
_args(command=['git', 'branch', '-a'], yes=True)),
(['thefuck', ARGUMENT_PLACEHOLDER, '-v'], _args(version=True)),
(['thefuck', ARGUMENT_PLACEHOLDER, '--help'], _args(help=True)),
(['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-y', '-d'],
_args(command=['git', 'branch', '-a'], yes=True, debug=True)),
(['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-r', '-d'],
_args(command=['git', 'branch', '-a'], repeat=True, debug=True))])
def test_parse(argv, result):
assert vars(Parser().parse(argv)) == result

View File

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

View File

@@ -1,4 +1,6 @@
import pytest
import json
from six import StringIO
from mock import MagicMock
from thefuck.shells.generic import ShellConfiguration
from thefuck.not_configured import main
@@ -11,19 +13,33 @@ def usage_tracker(mocker):
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 \
.open.return_value \
.__enter__.return_value \
.write.assert_called_once_with(str(pid))
.open.return_value \
.__enter__.return_value = io
return io
def _change_tracker(usage_tracker, pid):
usage_tracker.return_value.exists.return_value = True
@pytest.fixture(autouse=True)
def usage_tracker_exists(usage_tracker):
usage_tracker.return_value \
.open.return_value \
.__enter__.return_value \
.read.return_value = str(pid)
.exists.return_value = True
return usage_tracker.return_value.exists
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)
@@ -67,29 +83,28 @@ def test_for_generic_shell(shell, logs):
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
usage_tracker.return_value.exists.return_value = False
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()
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.get_history.return_value = ['fuck', 'ls']
_change_tracker(usage_tracker, 12)
_change_tracker(usage_tracker_io, 12)
main()
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.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 55)
main()
_assert_tracker_updated(usage_tracker, 12)
_assert_tracker_updated(usage_tracker_io, 12)
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()
def test_when_already_configured(usage_tracker, shell_pid,
def test_when_already_configured(usage_tracker_io, shell_pid,
shell, shell_config, logs):
shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 12)
_change_tracker(usage_tracker_io, 12)
shell_config.read.return_value = 'eval $(thefuck --alias)'
main()
logs.already_configured.assert_called_once()
def test_when_successfuly_configured(usage_tracker, shell_pid,
shell, shell_config, logs):
def test_when_successfully_configured(usage_tracker_io, shell_pid,
shell, shell_config, logs):
shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 12)
_change_tracker(usage_tracker_io, 12)
shell_config.read.return_value = ''
main()
shell_config.write.assert_any_call('eval $(thefuck --alias)')

View File

@@ -28,6 +28,20 @@ class TestCorrectedCommand(object):
assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \
u'CorrectedCommand(script=echo café, side_effect=None, priority=100)'
@pytest.mark.parametrize('script, printed, override_settings', [
('git branch', 'git branch', {'repeat': False, 'debug': False}),
('git brunch',
"git brunch || fuck --repeat --force-command 'git brunch'",
{'repeat': True, 'debug': False}),
('git brunch',
"git brunch || fuck --repeat --debug --force-command 'git brunch'",
{'repeat': True, 'debug': True})])
def test_run(self, capsys, settings, script, printed, override_settings):
settings.update(override_settings)
CorrectedCommand(script, None, 1000).run(Command())
out, _ = capsys.readouterr()
assert out[:-1] == printed
class TestRule(object):
def test_from_path(self, mocker):
@@ -96,16 +110,15 @@ class TestCommand(object):
Popen = Mock()
Popen.return_value.stdout.read.return_value = b'stdout'
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
@pytest.fixture(autouse=True)
def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.types.os.environ', {})
monkeypatch.setattr('thefuck.types.Command._wait_output',
staticmethod(lambda *_: True))
monkeypatch.setattr('thefuck.output_readers.rerun._wait_output',
lambda *_: True)
def test_from_script_calls(self, Popen, settings):
def test_from_script_calls(self, Popen, settings, os_environ):
settings.env = {}
assert Command.from_raw_script(
['apt-get', 'search', 'vim']) == Command(
@@ -115,7 +128,7 @@ class TestCommand(object):
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
env={})
env=os_environ)
@pytest.mark.parametrize('script, result', [
([''], None),

View File

@@ -69,34 +69,40 @@ class TestSelectCommand(object):
def test_without_confirmation(self, capsys, commands, settings):
settings.require_confirmation = False
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(
self, capsys, commands_with_side_effect, settings):
settings.require_confirmation = False
assert (ui.select_command(iter(commands_with_side_effect))
== 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):
patch_get_key(['\n'])
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):
patch_get_key([const.KEY_CTRL_C])
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,
commands_with_side_effect):
patch_get_key(['\n'])
assert (ui.select_command(iter(commands_with_side_effect))
== 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):
patch_get_key([const.KEY_DOWN, '\n'])
assert ui.select_command(iter(commands)) == commands[1]
assert capsys.readouterr() == (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n')
stderr = (
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')
@pytest.fixture(autouse=True)
def bins(self, mocker, monkeypatch):
monkeypatch.setattr('thefuck.conf.os.environ', {'PATH': 'path'})
def bins(self, mocker):
callables = list()
for name in ['diff', 'ls', 'café']:
bin_mock = mocker.Mock(name=name)

View File

@@ -0,0 +1,88 @@
import sys
from argparse import ArgumentParser, SUPPRESS
from .const import ARGUMENT_PLACEHOLDER
from .utils import get_alias
class Parser(object):
"""Argument parser that can handle arguments with our special
placeholder.
"""
def __init__(self):
self._parser = ArgumentParser(prog='thefuck', add_help=False)
self._add_arguments()
def _add_arguments(self):
"""Adds arguments to parser."""
self._parser.add_argument(
'-v', '--version',
action='store_true',
help="show program's version number and exit")
self._parser.add_argument(
'-a', '--alias',
nargs='?',
const=get_alias(),
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(
'-h', '--help',
action='store_true',
help='show this help message and exit')
self._add_conflicting_arguments()
self._parser.add_argument(
'-d', '--debug',
action='store_true',
help='enable debug output')
self._parser.add_argument(
'--force-command',
action='store',
help=SUPPRESS)
self._parser.add_argument(
'command',
nargs='*',
help='command that should be fixed')
def _add_conflicting_arguments(self):
"""It's too dangerous to use `-y` and `-r` together."""
group = self._parser.add_mutually_exclusive_group()
group.add_argument(
'-y', '--yes',
action='store_true',
help='execute fixed command without confirmation')
group.add_argument(
'-r', '--repeat',
action='store_true',
help='repeat on failure')
def _prepare_arguments(self, argv):
"""Prepares arguments by:
- removing placeholder and moving arguments after it to beginning,
we need this to distinguish arguments from `command` with ours;
- adding `--` before `command`, so our parse would ignore arguments
of `command`.
"""
if ARGUMENT_PLACEHOLDER in argv:
index = argv.index(ARGUMENT_PLACEHOLDER)
return argv[index + 1:] + ['--'] + argv[:index]
elif argv and not argv[0].startswith('-') and argv[0] != '--':
return ['--'] + argv
else:
return argv
def parse(self, argv):
arguments = self._prepare_arguments(argv[1:])
return self._parser.parse_args(arguments)
def print_usage(self):
self._parser.print_usage(sys.stderr)
def print_help(self):
self._parser.print_help(sys.stderr)

View File

@@ -14,7 +14,7 @@ class Settings(dict):
def __setattr__(self, key, value):
self[key] = value
def init(self):
def init(self, args=None):
"""Fills `settings` with values from `settings.py` and env."""
from .logs import exception
@@ -31,6 +31,8 @@ class Settings(dict):
except Exception:
exception("Can't load settings from env", sys.exc_info())
self.update(self._settings_from_args(args))
def _init_settings_file(self):
settings_path = self.user_dir.joinpath('settings.py')
if not settings_path.is_file():
@@ -96,7 +98,7 @@ class Settings(dict):
elif attr in ('wait_command', 'history_limit', 'wait_slow_command'):
return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history'):
'alter_history', 'instant_mode'):
return val.lower() == 'true'
elif attr == 'slow_commands':
return val.split(':')
@@ -109,5 +111,19 @@ class Settings(dict):
for env, attr in const.ENV_TO_ATTR.items()
if env in os.environ}
def _settings_from_args(self, args):
"""Loads settings from args."""
if not args:
return {}
from_args = {}
if args.yes:
from_args['require_confirmation'] = not args.yes
if args.debug:
from_args['debug'] = args.debug
if args.repeat:
from_args['repeat'] = args.repeat
return from_args
settings = Settings(const.DEFAULT_SETTINGS)

View File

@@ -34,6 +34,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_slow_command': 15,
'slow_commands': ['lein', 'react-native', 'gradle',
'./gradlew', 'vagrant'],
'repeat': False,
'instant_mode': False,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@@ -46,7 +48,9 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history',
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands'}
'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode'}
SETTINGS_HEADER = u"""# The Fuck settings file
#
@@ -59,3 +63,11 @@ SETTINGS_HEADER = u"""# The Fuck settings file
#
"""
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):
"""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
import colorama
from .conf import settings
from . import const
def color(color_):
@@ -16,6 +17,14 @@ def color(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):
sys.stderr.write(
u'{warn}[WARN] {title}:{reset}\n{trace}'
@@ -39,7 +48,8 @@ def failed(msg):
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,
side_effect=u' (+side effect)' if corrected_command.side_effect else u'',
bold=color(colorama.Style.BRIGHT),
@@ -48,9 +58,10 @@ def show_corrected_command(corrected_command):
def confirm_text(corrected_command):
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'/{red}ctrl+c{reset}]').format(
prefix=const.USER_COMMAND_MARK,
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r',
@@ -121,3 +132,9 @@ def configured_successfully(configuration_details):
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL),
reload=configuration_details.reload))
def version(thefuck_version, python_version):
sys.stderr.write(
u'The Fuck {} using Python {}\n'.format(thefuck_version,
python_version))

View File

@@ -3,26 +3,30 @@ from .system import init_output
init_output()
from argparse import ArgumentParser # noqa: E402
from pprint import pformat # noqa: E402
import sys # noqa: E402
import six # noqa: E402
from . import logs, types # noqa: E402
from .shells import shell # noqa: E402
from .conf import settings # noqa: E402
from .corrector import get_corrected_commands # noqa: E402
from .exceptions import EmptyCommand # noqa: E402
from .utils import get_installation_info, get_alias # noqa: E402
from .ui import select_command # noqa: E402
from .argument_parser import Parser # noqa: E402
from .utils import get_installation_info # noqa: E402
from .logs import warn # noqa: E402
def fix_command():
def fix_command(known_args):
"""Fixes previous command. Used when `thefuck` called without arguments."""
settings.init()
settings.init(known_args)
with logs.debug_time('Total'):
logs.debug(u'Run with settings: {}'.format(pformat(settings)))
raw_command = ([known_args.force_command] if known_args.force_command
else known_args.command)
try:
command = types.Command.from_raw_script(sys.argv[1:])
command = types.Command.from_raw_script(raw_command)
except EmptyCommand:
logs.debug('Empty command, nothing to do')
return
@@ -36,34 +40,31 @@ def fix_command():
sys.exit(1)
def print_alias():
"""Prints alias for current shell."""
try:
alias = sys.argv[2]
except IndexError:
alias = get_alias()
print(shell.app_alias(alias))
def main():
parser = ArgumentParser(prog='thefuck')
version = get_installation_info().version
parser.add_argument('-v', '--version',
action='version',
version='The Fuck {} using Python {}'.format(
version, sys.version.split()[0]))
parser.add_argument('-a', '--alias',
action='store_true',
help='[custom-alias-name] prints alias for current shell')
parser.add_argument('command',
nargs='*',
help='command that should be fixed')
known_args = parser.parse_args(sys.argv[1:2])
parser = Parser()
known_args = parser.parse(sys.argv)
if known_args.alias:
print_alias()
if known_args.help:
parser.print_help()
elif known_args.version:
logs.version(get_installation_info().version,
sys.version.split()[0])
elif known_args.command:
fix_command()
fix_command(known_args)
elif 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:
parser.print_usage()

View File

@@ -4,9 +4,11 @@ from .system import init_output
init_output()
import os # noqa: E402
from psutil import Process # noqa: E402
import json # noqa: E402
import time # 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 .conf import settings # noqa: E402
from .system import Path # noqa: E402
@@ -30,19 +32,41 @@ def _get_not_configured_usage_tracker_path():
def _record_first_run():
"""Records shell pid to tracker file."""
with _get_not_configured_usage_tracker_path().open('w') as tracker:
tracker.write(six.text_type(_get_shell_pid()))
info = {'pid': _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():
"""Returns `True` when we know that `fuck` called second time."""
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
current_pid = _get_shell_pid()
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):

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

@@ -6,12 +6,13 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
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
def get_new_command(command):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
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)

View File

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

View File

@@ -0,0 +1,14 @@
import re
from thefuck.specific.git import git_support
fix = u'git commit -m "Initial commit." && {command}'
refspec_does_not_match = re.compile(r'src refspec \w+ does not match any\.')
@git_support
def match(command):
return bool(refspec_does_not_match.search(command.stderr))
def get_new_command(command):
return fix.format(command=command.script)

View File

@@ -11,7 +11,7 @@ def match(command):
@git_support
def get_new_command(command):
return shell.and_('git add .', 'git stash pop', 'git reset .')
return shell.and_('git add --update', 'git stash pop', 'git reset .')
# make it come before the other applicable rules

View File

@@ -1,19 +1,11 @@
import re
from thefuck.utils import replace_command, for_app
from thefuck.utils import for_app
@for_app('heroku')
def match(command):
return 'is not a heroku command' in command.stderr and \
'Perhaps you meant' in command.stderr
def _get_suggests(stderr):
for line in stderr.split('\n'):
if 'Perhaps you meant' in line:
return re.findall(r'`([^`]+)`', line)
return 'Run heroku _ to run' in command.stderr
def get_new_command(command):
wrong = re.findall(r'`(\w+)` is not a heroku command', command.stderr)[0]
return replace_command(command, wrong, _get_suggests(command.stderr))
return re.findall('Run heroku _ to run ([^.]*)', command.stderr)[0]

View File

@@ -1,4 +1,4 @@
from thefuck.utils import get_all_executables, memoize, which
from thefuck.utils import get_all_executables, memoize
@memoize
@@ -9,10 +9,13 @@ def _get_executable(script_part):
def match(command):
return (not which(command.script_parts[0])
return (not command.script_parts[0] in get_all_executables()
and _get_executable(command.script_parts[0]))
def get_new_command(command):
executable = _get_executable(command.script_parts[0])
return command.script.replace(executable, u'{} '.format(executable), 1)
priority = 4000

View File

@@ -21,7 +21,8 @@ patterns = ['permission denied',
'edspermissionerror',
'you don\'t have write permissions',
'use `sudo`',
'SudoRequiredError']
'SudoRequiredError',
'error: insufficient privileges']
def match(command):

View File

@@ -9,6 +9,6 @@ def match(command):
def get_new_command(command):
broken = command.script_parts[1]
fix = re.findall(r'Did you mean `yarn ([^`]*)`', command.stderr)[0]
fix = re.findall(r'Did you mean [`"](?:yarn )?([^`"]*)[`"]', command.stderr)[0]
return replace_argument(command.script, broken, fix)

View File

@@ -1,6 +1,6 @@
import re
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.')
@@ -10,6 +10,9 @@ def match(command):
return regex.findall(command.stderr)
npm_commands = {'require': 'add'}
@eager
def _get_all_tasks():
proc = Popen(['yarn', '--help'], stdout=PIPE)
@@ -27,5 +30,9 @@ def _get_all_tasks():
def get_new_command(command):
misspelled_task = regex.findall(command.stderr)[0]
tasks = _get_all_tasks()
return replace_command(command, misspelled_task, tasks)
if misspelled_task in npm_commands:
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)

View File

@@ -0,0 +1,13 @@
import re
from thefuck.utils import for_app
regex = re.compile(r'Run "(.*)" instead')
@for_app('yarn', at_least=1)
def match(command):
return regex.findall(command.stderr)
def get_new_command(command):
return regex.findall(command.stderr)[0]

View File

@@ -1,22 +1,47 @@
import os
from uuid import uuid4
from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize
from .generic import Generic
class Bash(Generic):
def app_alias(self, fuck):
# It is VERY important to have the variables declared WITHIN the alias
alias = "alias {0}='TF_CMD=$(TF_ALIAS={0}" \
" PYTHONIOENCODING=utf-8" \
" TF_SHELL_ALIASES=$(alias)" \
" thefuck $(fc -ln -1)) &&" \
" eval $TF_CMD".format(fuck)
def app_alias(self, alias_name):
# It is VERY important to have the variables declared WITHIN the function
return '''
function {name} () {{
TF_PREVIOUS=$(fc -ln -1);
TF_PYTHONIOENCODING=$PYTHONIOENCODING;
export TF_ALIAS={name};
export TF_SHELL_ALIASES=$(alias);
export PYTHONIOENCODING=utf-8;
TF_CMD=$(
thefuck $TF_PREVIOUS {argument_placeholder} $@
) && eval $TF_CMD;
export PYTHONIOENCODING=$TF_PYTHONIOENCODING;
{alter_history}
}}
'''.format(
name=alias_name,
argument_placeholder=ARGUMENT_PLACEHOLDER,
alter_history=('history -s $TF_CMD;'
if settings.alter_history else ''))
if settings.alter_history:
return alias + "; history -s $TF_CMD'"
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 alias + "'"
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):
name, value = alias.replace('alias ', '', 1).split('=', 1)
@@ -41,7 +66,7 @@ class Bash(Generic):
if os.path.join(os.path.expanduser('~'), '.bashrc'):
config = '~/.bashrc'
elif os.path.join(os.path.expanduser('~'), '.bash_profile'):
config = '~/.bashrc'
config = '~/.bash_profile'
else:
config = 'bash config'

View File

@@ -18,7 +18,7 @@ class Fish(Generic):
default.add(alias.strip())
return default
def app_alias(self, fuck):
def app_alias(self, alias_name):
if settings.alter_history:
alter_history = (' builtin history delete --exact'
' --case-sensitive -- $fucked_up_command\n'
@@ -33,7 +33,7 @@ class Fish(Generic):
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n{1}'
' end\n'
'end').format(fuck, alter_history)
'end').format(alias_name, alter_history)
@memoize
@cache('.config/fish/config.fish', '.config/fish/functions')
@@ -66,6 +66,9 @@ class Fish(Generic):
def and_(self, *commands):
return u'; and '.join(commands)
def or_(self, *commands):
return u'; or '.join(commands)
def how_to_configure(self):
return self._create_shell_configuration(
content=u"eval (thefuck --alias | tr '\n' ';')",

View File

@@ -3,6 +3,7 @@ import os
import shlex
import six
from collections import namedtuple
from ..logs import warn
from ..utils import memoize
from ..conf import settings
from ..system import Path
@@ -32,9 +33,13 @@ class Generic(object):
"""Prepares command for running in shell."""
return command_script
def app_alias(self, fuck):
def app_alias(self, alias_name):
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):
return ''
@@ -66,6 +71,9 @@ class Generic(object):
def and_(self, *commands):
return u' && '.join(commands)
def or_(self, *commands):
return u' || '.join(commands)
def how_to_configure(self):
return
@@ -74,7 +82,7 @@ class Generic(object):
encoded = self.encode_utf8(command)
try:
splitted = shlex.split(encoded)
splitted = [s.replace("??", "\ ") for s in shlex.split(encoded.replace('\ ', '??'))]
except ValueError:
splitted = encoded.split(' ')

View File

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

View File

@@ -6,10 +6,10 @@ from .generic import Generic
class Tcsh(Generic):
def app_alias(self, fuck):
def app_alias(self, alias_name):
return ("alias {0} 'setenv TF_ALIAS {0} && "
"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):
name, value = alias.split("\t", 1)

View File

@@ -1,23 +1,46 @@
from time import time
import os
from uuid import uuid4
from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize
from .generic import Generic
class Zsh(Generic):
def app_alias(self, alias_name):
# It is VERY important to have the variables declared WITHIN the alias
alias = "alias {0}='TF_CMD=$(TF_ALIAS={0}" \
" PYTHONIOENCODING=utf-8" \
" TF_SHELL_ALIASES=$(alias)" \
" thefuck $(fc -ln -1 | tail -n 1)) &&" \
" eval $TF_CMD".format(alias_name)
# It is VERY important to have the variables declared WITHIN the function
return '''
{name} () {{
TF_PREVIOUS=$(fc -ln -1 | tail -n 1);
TF_CMD=$(
TF_ALIAS={name}
TF_SHELL_ALIASES=$(alias)
PYTHONIOENCODING=utf-8
thefuck $TF_PREVIOUS {argument_placeholder} $*
) && eval $TF_CMD;
{alter_history}
}}
'''.format(
name=alias_name,
argument_placeholder=ARGUMENT_PLACEHOLDER,
alter_history=('test -n "$TF_CMD" && print -s $TF_CMD'
if settings.alter_history else ''))
if settings.alter_history:
return alias + " ; test -n \"$TF_CMD\" && print -s $TF_CMD'"
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 alias + "'"
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):
name, value = alias.split('=', 1)

View File

@@ -1,14 +1,13 @@
from imp import load_source
from subprocess import Popen, PIPE
import os
import sys
import six
from psutil import Process, TimeoutExpired
from . import logs
from .shells import shell
from .conf import settings
from .const import DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand
from .utils import get_alias, format_raw_script
from .output_readers import get_output
class Command(object):
@@ -60,44 +59,6 @@ class Command(object):
kwargs.setdefault('stderr', self.stderr)
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
def from_raw_script(cls, raw_script):
"""Creates instance of `Command` from a list of script parts.
@@ -107,29 +68,13 @@ class Command(object):
:raises: EmptyCommand
"""
script = cls._prepare_script(raw_script)
script = format_raw_script(raw_script)
if not script:
raise EmptyCommand
env = dict(os.environ)
env.update(settings.env)
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)
expanded = shell.from_shell(script)
stdout, stderr = get_output(script, expanded)
return cls(expanded, stdout, stderr)
class Rule(object):
@@ -276,6 +221,22 @@ class CorrectedCommand(object):
return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
def _get_script(self):
"""Returns fixed commands script.
If `settings.repeat` is `True`, appends command with second attempt
of running fuck in case fixed command fails again.
"""
if settings.repeat:
repeat_fuck = '{} --repeat {}--force-command {}'.format(
get_alias(),
'--debug ' if settings.debug else '',
shell.quote(self.script))
return shell.or_(self.script, repeat_fuck)
else:
return self.script
def run(self, old_cmd):
"""Runs command from rule for passed command.
@@ -289,4 +250,5 @@ class CorrectedCommand(object):
# This depends on correct setting of PYTHONIOENCODING by the alias:
logs.debug(u'PYTHONIOENCODING: {}'.format(
os.environ.get('PYTHONIOENCODING', '!!not-set!!')))
print(self.script)
print(self._get_script())

View File

@@ -4,6 +4,7 @@ import sys
from .conf import settings
from .exceptions import NoRuleMatched
from .system import get_key
from .utils import get_alias
from . import logs, const
@@ -69,7 +70,8 @@ def select_command(corrected_commands):
try:
selector = CommandSelector(corrected_commands)
except NoRuleMatched:
logs.failed('No fucks given')
logs.failed('No fucks given' if get_alias() == 'fuck'
else 'Nothing found')
return
if not settings.require_confirmation:

View File

@@ -1,6 +1,5 @@
import os
import pickle
import pkg_resources
import re
import shelve
import six
@@ -8,7 +7,7 @@ from contextlib import closing
from decorator import decorator
from difflib import get_close_matches
from functools import wraps
from warnings import warn
from .logs import warn
from .conf import settings
from .system import Path
@@ -108,15 +107,16 @@ def get_all_executables():
return fallback
tf_alias = get_alias()
tf_entry_points = get_installation_info().get_entry_map()\
.get('console_scripts', {})\
.keys()
tf_entry_points = ['thefuck', 'fuck']
bins = [exe.name.decode('utf8') if six.PY2 else exe.name
for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)
and exe.name not in tf_entry_points]
aliases = [alias for alias in shell.get_aliases() if alias != tf_alias]
aliases = [alias.decode('utf8') if six.PY2 else alias
for alias in shell.get_aliases() if alias != tf_alias]
return bins + aliases
@@ -138,12 +138,17 @@ def eager(fn, *args, **kwargs):
@eager
def get_all_matched_commands(stderr, separator='Did you mean'):
if not isinstance(separator, list):
separator = [separator]
should_yield = False
for line in stderr.split('\n'):
if separator in line:
should_yield = True
elif should_yield and line:
yield line.strip()
for sep in separator:
if sep in line:
should_yield = True
break
else:
if should_yield and line:
yield line.strip()
def replace_command(command, broken, matched):
@@ -247,6 +252,8 @@ cache.disabled = False
def get_installation_info():
import pkg_resources
return pkg_resources.require('thefuck')[0]
@@ -275,3 +282,18 @@ def get_valid_history_without_current(command):
return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script
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]
ignore = E501,W503
exclude = venv,build,.tox
exclude = venv,build,.tox,setup.py,fastentrypoints.py