1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-10 11:56:08 +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 LICENSE.md
include fastentrypoints.py

View File

@@ -4,6 +4,8 @@ Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/) inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320). [tweet](https://twitter.com/liamosaur/status/506975850596536320).
The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode)
[![gif with examples][examples-link]][examples-link] [![gif with examples][examples-link]][examples-link]
Few more examples: Few more examples:
@@ -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.**
@@ -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_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` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` &ndash; runs `git pull` when `push` was rejected; * `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_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_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; * `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. * `workon_doesnt_exists` &ndash; fixes `virtualenvwrapper` env name os suggests to create new.
* `yarn_alias` &ndash; fixes aliased `yarn` commands like `yarn ls`; * `yarn_alias` &ndash; fixes aliased `yarn` commands like `yarn ls`;
* `yarn_command_not_found` &ndash; fixes misspelled `yarn` commands; * `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; * `yarn_help` &ndash; makes it easier to open `yarn` documentation;
Enabled by default only on specific platforms: 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. 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`.
@@ -387,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:
@@ -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-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

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

110
fastentrypoints.py Normal file
View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import os
import pytest import pytest
from thefuck import shells from thefuck import shells
from thefuck import conf, const from thefuck import conf, const
@@ -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

View File

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

View File

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

View File

@@ -55,4 +55,5 @@ def test_without_confirmation(proc, TIMEOUT):
@pytest.mark.functional @pytest.mark.functional
def test_how_to_configure_alias(proc, TIMEOUT): def test_how_to_configure_alias(proc, TIMEOUT):
proc.sendline(u'unfunction fuck')
how_to_configure(proc, TIMEOUT) 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) # (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', [

View File

@@ -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):

View File

@@ -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
''' '''

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): def test_get_new_command(stderr):
assert (get_new_command(Command('git stash pop', stderr=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 import pytest
from tests.utils import Command from tests.utils import Command
from thefuck.rules.heroku_not_command import match, get_new_command from thefuck.rules.heroku_not_command import match, get_new_command
def suggest_stderr(cmd): suggest_stderr = '''
return ''' ! `{}` is not a heroku command. log is not a heroku command.
! Perhaps you meant `logs`, `pg`. Perhaps you meant logs?
! See `heroku help` for a list of available commands.'''.format(cmd) ▸ Run heroku _ to run heroku logs.
▸ Run heroku help for a list of available commands.'''
no_suggest_stderr = ''' ! `aaaaa` is not a heroku command. @pytest.mark.parametrize('cmd', ['log'])
! See `heroku help` for a list of available commands.'''
@pytest.mark.parametrize('cmd', ['log', 'pge'])
def test_match(cmd): def test_match(cmd):
assert match( assert match(
Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd))) Command('heroku {}'.format(cmd), stderr=suggest_stderr))
@pytest.mark.parametrize('script, stderr', [ @pytest.mark.parametrize('script, stderr', [
('cat log', suggest_stderr('log')), ('cat log', suggest_stderr)])
('heroku aaa', no_suggest_stderr)])
def test_not_match(script, stderr): def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr)) assert not match(Command(script, stderr=stderr))
@pytest.mark.parametrize('cmd, result', [ @pytest.mark.parametrize('cmd, result', [
('log', ['heroku logs', 'heroku pg']), ('log', 'heroku logs')])
('pge', ['heroku pg', 'heroku logs'])])
def test_get_new_command(cmd, result): 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 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 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) @pytest.fixture(autouse=True)
def all_executables(mocker): def all_executables(mocker):
return mocker.patch( return mocker.patch(
@@ -23,11 +17,8 @@ def test_match(script):
assert match(Command(script)) assert match(Command(script))
@pytest.mark.parametrize('script, which_result', [ @pytest.mark.parametrize('script', ['git branch', 'vimfile'])
('git branch', '/usr/bin/git'), def test_not_match(script):
('vimfile', None)])
def test_not_match(script, which_result, which):
which.return_value = which_result
assert not match(Command(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_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`?' stderr_list = 'error Did you mean `yarn list`?'
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command(script='yarn rm', stderr=stderr_remove), Command(script='yarn rm', stderr=stderr_remove),
Command(script='yarn etil', stderr=stderr_etl),
Command(script='yarn ls', stderr=stderr_list)]) Command(script='yarn ls', stderr=stderr_list)])
def test_match(command): def test_match(command):
assert match(command) assert match(command)
@@ -17,6 +18,7 @@ def test_match(command):
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, new_command', [
(Command('yarn rm', stderr=stderr_remove), 'yarn remove'), (Command('yarn rm', stderr=stderr_remove), 'yarn remove'),
(Command('yarn etil', stderr=stderr_etl), 'yarn etl'),
(Command('yarn ls', stderr=stderr_list), 'yarn list')]) (Command('yarn ls', stderr=stderr_list), 'yarn list')])
def test_get_new_command(command, new_command): def test_get_new_command(command, new_command):
assert 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', [ @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

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) assert match(command)
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, url', [
(Command('yarn help clean', stdout=stdout_clean), (Command('yarn help clean', stdout=stdout_clean),
open_command('https://yarnpkg.com/en/docs/cli/clean'))]) 'https://yarnpkg.com/en/docs/cli/clean')])
def test_get_new_command(command, new_command): def test_get_new_command(command, url):
assert get_new_command(command) == new_command assert get_new_command(command) == open_command(url)

View File

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

View File

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

View File

@@ -18,6 +18,9 @@ class TestGeneric(object):
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd' 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): def test_get_aliases(self, shell):
assert shell.get_aliases() == {} assert shell.get_aliases() == {}

View File

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

View File

@@ -32,6 +32,9 @@ class TestZsh(object):
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd' 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): def test_get_aliases(self, shell):
assert shell.get_aliases() == { assert shell.get_aliases() == {
'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))', 'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))',
@@ -40,17 +43,17 @@ class TestZsh(object):
'll': 'ls -alF'} 'll': 'ls -alF'}
def test_app_alias(self, shell): def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck') assert 'fuck () {' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK') assert 'FUCK () {' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING' in shell.app_alias('fuck') assert 'PYTHONIOENCODING' in shell.app_alias('fuck')
def test_app_alias_variables_correctly_set(self, shell): def test_app_alias_variables_correctly_set(self, shell):
alias = shell.app_alias('fuck') alias = shell.app_alias('fuck')
assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias assert "fuck () {" in alias
assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias assert "TF_ALIAS=fuck" in alias
assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias assert 'PYTHONIOENCODING=utf-8' in alias
assert 'ALIASES=$(alias) thefuck' in alias assert 'TF_SHELL_ALIASES=$(alias)' in alias
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) 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') 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,12 +64,19 @@ 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']
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): class TestInitializeSettingsFile(object):
def test_ignore_if_exists(self, settings): def test_ignore_if_exists(self, settings):
settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) 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'), (False, '/user/test/config/', '/user/test/config/thefuck'),
(True, '~/.config', '~/.thefuck'), (True, '~/.config', '~/.thefuck'),
(True, '/user/test/config/', '~/.thefuck')]) (True, '/user/test/config/', '~/.thefuck')])
def test_get_user_dir_path(mocker, environ, settings, legacy_dir_exists, def test_get_user_dir_path(mocker, os_environ, settings, legacy_dir_exists,
xdg_config_home, result): xdg_config_home, result):
mocker.patch('thefuck.conf.Path.is_dir', mocker.patch('thefuck.conf.Path.is_dir',
return_value=legacy_dir_exists) return_value=legacy_dir_exists)
if xdg_config_home is not None: if xdg_config_home is not None:
environ['XDG_CONFIG_HOME'] = xdg_config_home os_environ['XDG_CONFIG_HOME'] = xdg_config_home
else: else:
environ.pop('XDG_CONFIG_HOME', None) os_environ.pop('XDG_CONFIG_HOME', None)
path = settings._get_user_dir_path().as_posix() path = settings._get_user_dir_path().as_posix()
assert path == os.path.expanduser(result) assert path == os.path.expanduser(result)

View File

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

View File

@@ -28,6 +28,20 @@ class TestCorrectedCommand(object):
assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \ assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \
u'CorrectedCommand(script=echo café, side_effect=None, priority=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): class TestRule(object):
def test_from_path(self, mocker): def test_from_path(self, mocker):
@@ -96,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(
@@ -115,7 +128,7 @@ class TestCommand(object):
stdin=PIPE, stdin=PIPE,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env={}) env=os_environ)
@pytest.mark.parametrize('script, result', [ @pytest.mark.parametrize('script, result', [
([''], None), ([''], None),

View File

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

View File

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

View File

@@ -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): def __setattr__(self, key, value):
self[key] = value self[key] = value
def init(self): def init(self, args=None):
"""Fills `settings` with values from `settings.py` and env.""" """Fills `settings` with values from `settings.py` and env."""
from .logs import exception from .logs import exception
@@ -31,6 +31,8 @@ class Settings(dict):
except Exception: except Exception:
exception("Can't load settings from env", sys.exc_info()) exception("Can't load settings from env", sys.exc_info())
self.update(self._settings_from_args(args))
def _init_settings_file(self): def _init_settings_file(self):
settings_path = self.user_dir.joinpath('settings.py') settings_path = self.user_dir.joinpath('settings.py')
if not settings_path.is_file(): if not settings_path.is_file():
@@ -96,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(':')
@@ -109,5 +111,19 @@ class Settings(dict):
for env, attr in const.ENV_TO_ATTR.items() for env, attr in const.ENV_TO_ATTR.items()
if env in os.environ} 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) settings = Settings(const.DEFAULT_SETTINGS)

View File

@@ -34,6 +34,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_slow_command': 15, 'wait_slow_command': 15,
'slow_commands': ['lein', 'react-native', 'gradle', 'slow_commands': ['lein', 'react-native', 'gradle',
'./gradlew', 'vagrant'], './gradlew', 'vagrant'],
'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',
@@ -46,7 +48,9 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_HISTORY_LIMIT': 'history_limit', 'THEFUCK_HISTORY_LIMIT': 'history_limit',
'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_INSTANT_MODE': 'instant_mode'}
SETTINGS_HEADER = u"""# The Fuck settings file 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): class NoRuleMatched(Exception):
"""Raised when no rule matched for some command.""" """Raised when no rule matched for some command."""
class ScriptNotInLog(Exception):
"""Script not found in log."""

View File

@@ -6,6 +6,7 @@ import sys
from traceback import format_exception from traceback import format_exception
import colorama import colorama
from .conf import settings from .conf import settings
from . import const
def color(color_): def color(color_):
@@ -16,6 +17,14 @@ def color(color_):
return color_ return color_
def warn(title):
sys.stderr.write(u'{warn}[WARN] {title}{reset}\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL),
title=title))
def exception(title, exc_info): def exception(title, exc_info):
sys.stderr.write( sys.stderr.write(
u'{warn}[WARN] {title}:{reset}\n{trace}' u'{warn}[WARN] {title}:{reset}\n{trace}'
@@ -39,7 +48,8 @@ def failed(msg):
def show_corrected_command(corrected_command): def show_corrected_command(corrected_command):
sys.stderr.write(u'{bold}{script}{reset}{side_effect}\n'.format( sys.stderr.write(u'{prefix}{bold}{script}{reset}{side_effect}\n'.format(
prefix=const.USER_COMMAND_MARK,
script=corrected_command.script, script=corrected_command.script,
side_effect=u' (+side effect)' if corrected_command.side_effect else u'', side_effect=u' (+side effect)' if corrected_command.side_effect else u'',
bold=color(colorama.Style.BRIGHT), bold=color(colorama.Style.BRIGHT),
@@ -48,9 +58,10 @@ def show_corrected_command(corrected_command):
def confirm_text(corrected_command): def confirm_text(corrected_command):
sys.stderr.write( sys.stderr.write(
(u'{clear}{bold}{script}{reset}{side_effect} ' (u'{prefix}{clear}{bold}{script}{reset}{side_effect} '
u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}' u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
u'/{red}ctrl+c{reset}]').format( u'/{red}ctrl+c{reset}]').format(
prefix=const.USER_COMMAND_MARK,
script=corrected_command.script, script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '', side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r', clear='\033[1K\r',
@@ -121,3 +132,9 @@ def configured_successfully(configuration_details):
bold=color(colorama.Style.BRIGHT), bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL), reset=color(colorama.Style.RESET_ALL),
reload=configuration_details.reload)) 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() init_output()
from argparse import ArgumentParser # noqa: E402
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
from .corrector import get_corrected_commands # noqa: E402 from .corrector import get_corrected_commands # noqa: E402
from .exceptions import EmptyCommand # 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 .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.""" """Fixes previous command. Used when `thefuck` called without arguments."""
settings.init() settings.init(known_args)
with logs.debug_time('Total'): with logs.debug_time('Total'):
logs.debug(u'Run with settings: {}'.format(pformat(settings))) 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: try:
command = types.Command.from_raw_script(sys.argv[1:]) command = types.Command.from_raw_script(raw_command)
except EmptyCommand: except EmptyCommand:
logs.debug('Empty command, nothing to do') logs.debug('Empty command, nothing to do')
return return
@@ -36,34 +40,31 @@ def fix_command():
sys.exit(1) 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(): def main():
parser = ArgumentParser(prog='thefuck') parser = Parser()
version = get_installation_info().version known_args = parser.parse(sys.argv)
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])
if known_args.alias: if known_args.help:
print_alias() parser.print_help()
elif known_args.version:
logs.version(get_installation_info().version,
sys.version.split()[0])
elif known_args.command: 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: else:
parser.print_usage() parser.print_usage()

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -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 @git_support
def get_new_command(command): 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 # make it come before the other applicable rules

View File

@@ -1,19 +1,11 @@
import re import re
from thefuck.utils import replace_command, for_app from thefuck.utils import for_app
@for_app('heroku') @for_app('heroku')
def match(command): def match(command):
return 'is not a heroku command' in command.stderr and \ return 'Run heroku _ to run' in command.stderr
'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)
def get_new_command(command): def get_new_command(command):
wrong = re.findall(r'`(\w+)` is not a heroku command', command.stderr)[0] return re.findall('Run heroku _ to run ([^.]*)', command.stderr)[0]
return replace_command(command, wrong, _get_suggests(command.stderr))

View File

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

View File

@@ -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):

View File

@@ -9,6 +9,6 @@ def match(command):
def get_new_command(command): def get_new_command(command):
broken = command.script_parts[1] 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) return replace_argument(command.script, broken, fix)

View File

@@ -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)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +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, format_raw_script
from .output_readers import get_output
class Command(object): class Command(object):
@@ -60,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.
@@ -107,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):
@@ -276,6 +221,22 @@ class CorrectedCommand(object):
return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format( return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority) 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): def run(self, old_cmd):
"""Runs command from rule for passed command. """Runs command from rule for passed command.
@@ -289,4 +250,5 @@ class CorrectedCommand(object):
# This depends on correct setting of PYTHONIOENCODING by the alias: # This depends on correct setting of PYTHONIOENCODING by the alias:
logs.debug(u'PYTHONIOENCODING: {}'.format( logs.debug(u'PYTHONIOENCODING: {}'.format(
os.environ.get('PYTHONIOENCODING', '!!not-set!!'))) 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 .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:

View File

@@ -1,6 +1,5 @@
import os import os
import pickle import pickle
import pkg_resources
import re import re
import shelve import shelve
import six import six
@@ -8,7 +7,7 @@ from contextlib import closing
from decorator import decorator from decorator import decorator
from difflib import get_close_matches from difflib import get_close_matches
from functools import wraps from functools import wraps
from warnings import warn from .logs import warn
from .conf import settings from .conf import settings
from .system import Path from .system import Path
@@ -108,15 +107,16 @@ 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(':')
for exe in _safe(lambda: list(Path(path).iterdir()), []) for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True) if not _safe(exe.is_dir, True)
and exe.name not in tf_entry_points] 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 return bins + aliases
@@ -138,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):
@@ -247,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]
@@ -275,3 +282,18 @@ def get_valid_history_without_current(command):
return [line for line in _not_corrected(history, tf_alias) return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script if not line.startswith(tf_alias) and not line == command.script
and line.split(' ')[0] in executables] and line.split(' ')[0] in executables]
def format_raw_script(raw_script):
"""Creates single script from a list of script parts.
:type raw_script: [basestring]
:rtype: basestring
"""
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in raw_script)
else:
script = ' '.join(raw_script)
return script.strip()

View File

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