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

Compare commits

...

86 Commits

Author SHA1 Message Date
nvbn
bd5cf38271 Bump to 3.3 2016-01-13 22:33:04 +03:00
Vladimir Iakovlev
3c3d17e0ea Merge pull request #432 from nvbn/422-not-alter-history
#422: Add `alter_history` settings option
2016-01-13 22:31:19 +03:00
nvbn
2f353498de #422: Add alter_history settings option 2016-01-13 22:22:51 +03:00
Vladimir Iakovlev
f0f49c1865 Merge pull request #430 from nvbn/429-apt-invalid-operation
#429: Add `apt_invalid_operation` rule
2016-01-13 22:12:35 +03:00
nvbn
20fff3142c #429: Fix tests with python 2 2016-01-13 22:08:24 +03:00
Vladimir Iakovlev
6e22b9ec6c Merge pull request #431 from nvbn/428-readonly-history
#428: Don't fail when history is readonly
2016-01-13 22:03:31 +03:00
nvbn
d53240b777 #428: Don't fail when history is readonly 2016-01-13 22:00:20 +03:00
nvbn
cab933e7e6 #429: Add apt_invalid_operation rule 2016-01-13 21:53:11 +03:00
Vladimir Iakovlev
8b05f6d46f Merge pull request #427 from makalaaneesh/master
#425 command had to be re escaped
2016-01-08 16:38:05 +03:00
Vladimir Iakovlev
ec64fbd5ea Merge pull request #426 from web-connect/patch-1
Fixing typos
2016-01-08 16:37:23 +03:00
makalaaneesh
4f9fb796c4 fixes #425. command had to be re escaped 2016-01-08 00:50:26 +05:30
Justin Turner
be744f20ba Fixing typos 2016-01-07 09:36:37 -06:00
Vladimir Iakovlev
1b12cd85e9 Merge pull request #423 from MattKotsenas/bugfix/cd_mkdir
Add Windows error message support to cd_mkdir rule
2016-01-06 03:34:03 +03:00
Matt Kotsenas
47df80f6b8 Add Windows error message support to cd_mkdir rule
Add the Windows error message 'the system cannot find the path specified'
to the list of recognized messages for cd_mkdir.
2016-01-05 13:55:59 -08:00
Vladimir Iakovlev
a0ef0efe46 Merge pull request #419 from mcarton/fix-unzip
Fix the `dirty_unzip` rule
2015-12-30 00:58:30 +03:00
Vladimir Iakovlev
25662ad737 Merge pull request #418 from makalaaneesh/master
sudo sh execute for && in commands - preventing double sudo
2015-12-30 00:57:50 +03:00
mcarton
42b344676e Fix dirty_unzip rule on non-zip files 2015-12-29 18:46:35 +01:00
mcarton
a3e1cb6718 Fix thefuck unzip, fix #416 2015-12-29 18:38:58 +01:00
makalaaneesh
f249098336 sudo sh execute for && in commands - preventing double sudo 2015-12-23 14:35:47 +05:30
nvbn
c3b1ba7637 #415: Prevent double sudo 2015-12-11 07:41:13 +08:00
nvbn
b65a9a0a4f #414: Initialize output before any colorama import 2015-12-04 18:34:52 +08:00
nvbn
29c1d1efcf #414: Move system-dependent utils in system module 2015-12-03 20:03:27 +08:00
nvbn
0560f4ba8e #414: Install and use win_unicode_console only on windows 2015-12-01 20:15:27 +08:00
Pavel Krymets
f9aa0e7c6b Fix windows unicode output issues 2015-11-30 16:24:31 -08:00
Pavel Krymets
b18a049886 Fix getch on windows 2015-11-30 12:33:28 -08:00
nvbn
9192b555b5 Merge branch 'master' of github.com:nvbn/thefuck 2015-11-26 03:42:16 +08:00
nvbn
d750d3d6d1 #412: Add _script_from_history for generic shell 2015-11-26 03:42:03 +08:00
Vladimir Iakovlev
3ad953001d Merge pull request #411 from scorphus/unicode
Support non-ascii content in Python 2
2015-11-25 20:41:20 +08:00
Pablo Santiago Blum de Aguiar
3b4b87d8ed #398: Test PYTHONIOENCODING=utf-8 in shell aliases 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
6c3d67763a #398: Add PYTHONIOENCODING=utf-8 to Fish Shell alias 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
959680d24d #N/A Set TF_ALIAS as an environment variable
For more info, check:

http://fishshell.com/docs/current/faq.html#faq-single-env
2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
b0adc7f2ca #N/A Indent Fish alias with two spaces (default) 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
fc05364233 #398 & #408: Support non-ascii IO in Python 2 2015-11-25 02:34:19 -02:00
Pablo Santiago Blum de Aguiar
ad3db4ac67 #N/A Fix F812 list comprehension redefines cmd 2015-11-25 02:34:15 -02:00
Pablo Santiago Blum de Aguiar
4a7b335d7c #N/A Add ability to get Fish Shell history 2015-11-25 02:34:02 -02:00
Pablo Santiago Blum de Aguiar
465f6191b0 #N/A Cleanup and adjust syntax 2015-11-25 01:58:07 -02:00
Vladimir Iakovlev
b2836319ad Update README.md 2015-11-19 11:22:56 +08:00
Vladimir Iakovlev
b3e9b36bd1 Merge pull request #409 from nvbn/394-history-limit
#394 history limit
2015-11-19 11:17:21 +08:00
lovedboy
ae2949cfa2 python2.7 unicode error 2015-11-19 09:40:44 +08:00
nvbn
1bb04b41eb #398: Add PYTHONIOENCODING=utf-8 to shell aliases 2015-11-18 18:37:11 +08:00
Vladimir Iakovlev
acd0b3e024 Merge pull request #406 from mcarton/py2→3
Fix cache problem when going from Python 2 to 3
2015-11-18 18:32:24 +08:00
mcarton
7c5676491a Fix some more warnings from flake8 2015-11-15 18:08:59 +01:00
mcarton
8feb722ed0 Fix some pep8 warnings 2015-11-15 18:02:37 +01:00
mcarton
c3ea2fd0c7 Fix cache problem when going from Python 2 to 3 2015-11-15 16:55:07 +01:00
nvbn
b55464b2ea #403 Add sudo rule's pattern for dscl 2015-11-13 15:37:13 +08:00
nvbn
8ddb61ae89 #N/A Add python-gdbm to install script 2015-11-12 18:43:15 +08:00
Vladimir Iakovlev
fe91008a9c Merge pull request #400 from alessio/fix-memoize
Fix misinterpretation of the disabled flag
2015-11-06 02:19:07 +08:00
Alessio Treglia
7f777213c5 Fix misinterpretation of the disabled flag
The old implementation was misinterpretating the disabled flag and
effectively applying memoization even when explicitly disabled.
The 'or' operator is a short-circuit one; namely, it evaluates the
second argument if and only if the first is False. Therefore the
following conditions caused unexpected side effects:

- memoize.disabled = True, key not yet memoized

  Having disabled the memoize function wrapper, the client expects
  that no memoization happens. Instead the execution enters the
  if clause and store the value into the 'memo' dictionary

- memoize.disabled = True, key memoized

  Having disabled the memoize function wrapper, the client expects
  that no memoization happens and the function will be evaluated
  anyway, whether or not its return value had already been stored in
  the 'memo' dictionary by a previous call. On the contrary, the last
  statement of wrapper() access the value stored by the last function
  execution.

This commit attempts to improve the function readability too.
2015-11-04 22:44:50 +00:00
nvbn
89f868c115 Bump to 3.2 2015-11-03 17:38:07 +08:00
nvbn
81f6a25abc #398: Fix UnicodeDecodeError in logs 2015-11-01 14:42:48 +08:00
nvbn
cc9af78787 Merge branch 'master' of github.com:nvbn/thefuck 2015-11-01 13:17:15 +08:00
nvbn
1fc3f1b5bf #398: Fix UnicodeDecodeError in logs 2015-11-01 13:16:58 +08:00
Vladimir Iakovlev
45574d06c9 Merge pull request #397 from janek-warchol/use-force-with-lease
Use --force-with-lease instead of --force for git push
2015-10-31 02:47:51 +08:00
Jan Warchoł
dc23d67a42 Use --force-with-lease instead of --force for git push
--force flag can be very dangerous, because it unconditionally
overwrites remote branch - if someone pushed new commits to the remote
repo after you last fetched/pulled, and you do push --force, you will
overwrite his commits without even knowing that you did that.  Using
--force-with-lease is much safer because it only overwrites remote
branch when it points to the same commit that you think it points to.

Read more:
https://developer.atlassian.com/blog/2015/04/force-with-lease/
2015-10-30 16:17:56 +01:00
nvbn
2fea0d4c60 #394: Force history_limit to be int 2015-10-30 16:23:19 +08:00
nvbn
8c8abca8d5 #394: readlines isn't lazy 2015-10-29 22:51:30 +08:00
nvbn
bd6ee68c03 #394: Try simpler solution to limit lines count 2015-10-29 20:17:17 +08:00
nvbn
16533e85a7 Merge branch 'debug' of git://github.com/lovedboy/thefuck into lovedboy-debug 2015-10-29 20:00:04 +08:00
lovedboy
b3a19fe439 history limit from settings 2015-10-29 10:14:34 +08:00
nvbn
959b96cf6e #392: Show only debug message if script isn't splitable 2015-10-29 01:03:27 +08:00
nvbn
f20311fa89 #392: Little refactoring 2015-10-29 00:13:59 +08:00
nvbn
a4c391096a Merge branch 'fix-split' of https://github.com/mcarton/thefuck into mcarton-fix-split 2015-10-29 00:04:29 +08:00
mcarton
e71a3e0cdb Replace (almost) all instance of script.split 2015-10-28 16:43:24 +01:00
mcarton
2d995d464f Fix the cpp11 rule 2015-10-28 15:27:10 +01:00
mcarton
280751b36e Fix the unzip rules and filenames with spaces 2015-10-28 15:13:33 +01:00
mcarton
0a6a3db65d Fix the untar rules and filenames with spaces 2015-10-28 15:12:59 +01:00
mcarton
ecfc180280 Add shells.quote 2015-10-28 14:16:01 +01:00
mcarton
dae58211ba Parse command line with shlex
I put that in shells so that weird shells might try to parse it
differently.
2015-10-28 14:01:14 +01:00
Vladimir Iakovlev
5e9b2c56da Merge pull request #391 from mcarton/tox-3.5
#374 Test python 3.5 with tox
2015-10-28 20:52:45 +08:00
mcarton
192ab0bfb0 Test python 3.5 with tox 2015-10-28 13:32:37 +01:00
lovedboy
372e983459 add THEFUCK_HISTORY_LIMIT, my machine is so slow 2015-10-22 19:25:00 +08:00
nvbn
346cb99217 #385 Little refactoring 2015-10-21 18:13:22 +08:00
nvbn
bbfedb861f Merge branch 'xdg' of https://github.com/mcarton/thefuck into mcarton-xdg 2015-10-21 18:03:39 +08:00
nvbn
f5b0e96747 #382 Prevent partial execution of install.sh 2015-10-21 18:00:08 +08:00
Vladimir Iakovlev
12a33f56bc Merge pull request #389 from scorphus/fix-touch
Fix `rules.touch` tests
2015-10-21 17:50:09 +08:00
Pablo Santiago Blum de Aguiar
590fdba2aa Fix rules.touch tests
Move them to `rules` sub-directory and import `shells` instead of `and_`
which in turn triggers the `generic_shell` fixture fixing the tests on
Fish Shell.
2015-10-18 19:49:46 -02:00
Vladimir Iakovlev
f374142bf8 Merge pull request #384 from scorphus/fish-func
Improve the Fish Shell function making it faster
2015-10-19 01:50:53 +08:00
Vladimir Iakovlev
540ff7e16d Merge pull request #387 from scorphus/git-two-dashes
Add `git_two_dashes` rule
2015-10-19 01:50:04 +08:00
Vladimir Iakovlev
806dad18bf Merge pull request #386 from mcarton/CONTRIBUTING
Add a CONTRIBUTING file
2015-10-19 01:49:02 +08:00
Vladimir Iakovlev
8b416f269f Merge pull request #388 from scorphus/fix-brew-tests
Fix rules.brew_install tests on Mac
2015-10-19 01:48:53 +08:00
Pablo Santiago Blum de Aguiar
5e44fb22be Fix rules.brew_install tests on Mac 2015-10-17 18:46:07 -03:00
Pablo Santiago Blum de Aguiar
5389d0c106 Add git_two_dashes rule 2015-10-17 18:40:53 -03:00
mcarton
c970f190d2 Add a CONTRIBUTING file 2015-10-17 15:45:13 +02:00
mcarton
8f25c95f06 Use XDG_CACHE_HOME for cache 2015-10-16 17:33:52 +02:00
mcarton
4a48108c69 Follow the XDG Base Directory Specification 2015-10-16 16:52:03 +02:00
Pablo Santiago Blum de Aguiar
f5e8fe954e Improve the Fish Shell function making it faster
It works now with no temp file involved, which makes it a lot faster.
Also, `history --merge`, although only supported on Fish Shell 2.2+,
merges the corrected entry back into history. Neat!

Ref #89
2015-10-16 00:42:34 -03:00
77 changed files with 970 additions and 420 deletions

25
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,25 @@
# Report issues
If you have any issue with The Fuck, sorry about that, but we will do what we
can to fix that. Actually, maybe we already have, so first thing to do is to
update The Fuck and see if the bug is still there.
If it is (sorry again), check if the problem has not already been reported and
if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with
the following basic information:
- the output of `thefuck --version` (something like `The Fuck 3.1 using
Python 3.5.0`);
- your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.);
- your system (Debian 7, ArchLinux, Windows, etc.);
- how to reproduce the bug;
- the output of The Fuck with `THEFUCK_DEBUG=true` exported (typically execute
`export THEFUCK_DEBUG=true` in your shell before The Fuck);
- if the bug only appears with a specific application, the output of that
application and its version;
- anything else you think is relevant.
It's only with enough information that we can do something to fix the problem.
# Make a pull request
We gladly accept pull request on the [official
repository](https://github.com/nvbn/thefuck) for new rules, new features, bug
fixes, etc.

View File

@@ -167,6 +167,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` – runs `git pull` when `push` was rejected;
* `git_stash` – stashes you local modifications before rebasing or switching branch;
* `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`;
* `go_run` – appends `.go` extension when compiling/running Go programs
* `grep_recursive` – adds `-r` when you trying to `grep` directory;
* `gulp_not_task` – fixes misspelled `gulp` tasks;
@@ -210,6 +211,7 @@ Enabled by default only on specific platforms:
* `apt_get` – installs app from apt if it not installed (requires `python-commandnotfound` / `python3-commandnotfound`);
* `apt_get_search` – changes trying to search using `apt-get` with searching using `apt-cache`;
* `apt_invalid_operation` – fixes invalid `apt` and `apt-get` calls, like `apt-get isntall vim`;
* `brew_install` – fixes formula name for `brew install`;
* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_upgrade` – appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
@@ -218,13 +220,13 @@ Enabled by default only on specific platforms:
Bundled, but not enabled by default:
* `git_push_force` – adds `--force` to a `git push` (may conflict with `git_push_pull`);
* `git_push_force` – adds `--force-with-lease` to a `git push` (may conflict with `git_push_pull`);
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
in `~/.thefuck/rules`. The rule should contain two functions:
in `~/.config/thefuck/rules`. The rule should contain two functions:
```python
match(command: Command) -> bool
@@ -241,7 +243,7 @@ and optional `enabled_by_default`, `requires_output` and `priority` variables.
`Command` has three attributes: `script`, `stdout` and `stderr`.
*Rules api changed in 3.0:* For accessing settings in rule you need to import it with `from thefuck.conf import settings`.
`settings` is a special object filled with `~/.thefuck/settings.py` and values from env ([see more below](#settings)).
`settings` is a special object filled with `~/.config/thefuck/settings.py` and values from env ([see more below](#settings)).
Simple example of the rule for running script with `sudo`:
@@ -271,7 +273,7 @@ requires_output = True
## Settings
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME/thefuck/settings.py` (`$XDG_CONFIG_HOME` defaults to `~/.config`):
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `exclude_rules` – list of disabled rules, by default `[]`;
@@ -279,7 +281,9 @@ The Fuck has a few settings parameters which can be changed in `~/.thefuck/setti
* `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` – enables debug output, by default `False`.
* `debug` – enables debug output, by default `False`;
* `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` – push fixed command to history, by default `True`.
Example of `settings.py`:
@@ -302,7 +306,9 @@ Or via environment variables:
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first;
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
* `THEFUCK_DEBUG` – enables debug output, `true/false`;
* `THEFUCK_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`.
For example:
@@ -313,6 +319,7 @@ export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000'
```
## Developing

View File

@@ -8,50 +8,54 @@ installed () {
hash $1 2>/dev/null
}
# Install os dependencies:
if installed apt-get; then
# Debian/ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found
install_thefuck () {
# Install OS dependencies:
if installed apt-get; then
# Debian/Ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found python-gdbm
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
else
if installed brew; then
# OS X:
brew update
brew install python
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
else
# Genreic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
if installed brew; then
# OS X:
brew update
brew install python
else
# Generic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
fi
fi
fi
# thefuck requires fresh versions of setuptools and pip:
sudo pip install -U pip setuptools
sudo pip install -U thefuck
# thefuck requires fresh versions of setuptools and pip:
sudo pip install -U pip setuptools
sudo pip install -U thefuck
# Setup aliases:
if should_add_alias ~/.bashrc; then
echo 'eval $(thefuck --alias)' >> ~/.bashrc
fi
# Setup aliases:
if should_add_alias ~/.bashrc; then
echo 'eval $(thefuck --alias)' >> ~/.bashrc
fi
if should_add_alias ~/.bash_profile; then
echo 'eval $(thefuck --alias)' >> ~/.bash_profile
fi
if should_add_alias ~/.bash_profile; then
echo 'eval $(thefuck --alias)' >> ~/.bash_profile
fi
if should_add_alias ~/.zshrc; then
echo 'eval $(thefuck --alias)' >> ~/.zshrc
fi
if should_add_alias ~/.zshrc; then
echo 'eval $(thefuck --alias)' >> ~/.zshrc
fi
if should_add_alias ~/.config/fish/config.fish; then
thefuck --alias >> ~/.config/fish/config.fish
fi
if should_add_alias ~/.config/fish/config.fish; then
thefuck --alias >> ~/.config/fish/config.fish
fi
if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi
if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi
}
install_thefuck

View File

@@ -20,10 +20,11 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.1'
VERSION = '3.3'
install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib']}
extras_require = {':python_version<"3.4"': ['pathlib'],
":sys_platform=='win32'": ['win_unicode_console']}
setup(name='thefuck',
version=VERSION,

View File

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

View File

@@ -0,0 +1,122 @@
from io import BytesIO
import pytest
from tests.utils import Command
from thefuck.rules.apt_invalid_operation import match, get_new_command, \
_get_operations
invalid_operation = 'E: Invalid operation {}'.format
apt_help = b'''apt 1.0.10.2ubuntu1 for amd64 compiled on Oct 5 2015 15:55:05
Usage: apt [options] command
CLI for apt.
Basic commands:
list - list packages based on package names
search - search in package descriptions
show - show package details
update - update list of available packages
install - install packages
remove - remove packages
upgrade - upgrade the system by installing/upgrading packages
full-upgrade - upgrade the system by removing/installing/upgrading packages
edit-sources - edit the source information file
'''
apt_operations = ['list', 'search', 'show', 'update', 'install', 'remove',
'upgrade', 'full-upgrade', 'edit-sources']
apt_get_help = b'''apt 1.0.10.2ubuntu1 for amd64 compiled on Oct 5 2015 15:55:05
Usage: apt-get [options] command
apt-get [options] install|remove pkg1 [pkg2 ...]
apt-get [options] source pkg1 [pkg2 ...]
apt-get is a simple command line interface for downloading and
installing packages. The most frequently used commands are update
and install.
Commands:
update - Retrieve new lists of packages
upgrade - Perform an upgrade
install - Install new packages (pkg is libc6 not libc6.deb)
remove - Remove packages
autoremove - Remove automatically all unused packages
purge - Remove packages and config files
source - Download source archives
build-dep - Configure build-dependencies for source packages
dist-upgrade - Distribution upgrade, see apt-get(8)
dselect-upgrade - Follow dselect selections
clean - Erase downloaded archive files
autoclean - Erase old downloaded archive files
check - Verify that there are no broken dependencies
changelog - Download and display the changelog for the given package
download - Download the binary package into the current directory
Options:
-h This help text.
-q Loggable output - no progress indicator
-qq No output except for errors
-d Download only - do NOT install or unpack archives
-s No-act. Perform ordering simulation
-y Assume Yes to all queries and do not prompt
-f Attempt to correct a system with broken dependencies in place
-m Attempt to continue if archives are unlocatable
-u Show a list of upgraded packages as well
-b Build the source package after fetching it
-V Show verbose version numbers
-c=? Read this configuration file
-o=? Set an arbitrary configuration option, eg -o dir::cache=/tmp
See the apt-get(8), sources.list(5) and apt.conf(5) manual
pages for more information and options.
This APT has Super Cow Powers.
'''
apt_get_operations = ['update', 'upgrade', 'install', 'remove', 'autoremove',
'purge', 'source', 'build-dep', 'dist-upgrade',
'dselect-upgrade', 'clean', 'autoclean', 'check',
'changelog', 'download']
@pytest.mark.parametrize('script, stderr', [
('apt', invalid_operation('saerch')),
('apt-get', invalid_operation('isntall')),
('apt-cache', invalid_operation('rumove'))])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr', [
('vim', invalid_operation('vim')),
('apt-get', "")])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr))
@pytest.fixture
def set_help(mocker):
mock = mocker.patch('subprocess.Popen')
def _set_text(text):
mock.return_value.stdout = BytesIO(text)
return _set_text
@pytest.mark.parametrize('app, help_text, operations', [
('apt', apt_help, apt_operations),
('apt-get', apt_get_help, apt_get_operations)
])
def test_get_operations(set_help, app, help_text, operations):
set_help(help_text)
assert _get_operations(app) == operations
@pytest.mark.parametrize('script, stderr, help_text, result', [
('apt-get isntall vim', invalid_operation('isntall'),
apt_get_help, 'apt-get install vim'),
('apt saerch vim', invalid_operation('saerch'),
apt_help, 'apt search vim'),
])
def test_get_new_command(set_help, stderr, script, help_text, result):
set_help(help_text)
assert get_new_command(Command(script, stderr=stderr))[0] == result

View File

@@ -31,8 +31,7 @@ def test_match(brew_no_available_formula, brew_already_installed,
stderr=brew_no_available_formula))
assert not match(Command('brew install git',
stderr=brew_already_installed))
assert not match(Command('brew install', stderr=brew_install_no_argument),
None)
assert not match(Command('brew install', stderr=brew_install_no_argument))
@pytest.mark.skipif(_is_not_okay_to_test(),
@@ -43,5 +42,5 @@ def test_get_new_command(brew_no_available_formula):
== 'brew install elasticsearch'
assert get_new_command(Command('brew install aa',
stderr=brew_no_available_formula),
None) != 'brew install aha'
stderr=brew_no_available_formula))\
!= 'brew install aha'

View File

@@ -1,7 +1,8 @@
import os
import pytest
import tarfile
from thefuck.rules.dirty_untar import match, get_new_command, side_effect
from thefuck.rules.dirty_untar import match, get_new_command, side_effect, \
tar_extensions
from tests.utils import Command
@@ -32,34 +33,41 @@ def tar_error(tmpdir):
return fixture
parametrize_filename = pytest.mark.parametrize('filename', [
'foo.tar',
'foo.tar.gz',
'foo.tgz'])
parametrize_extensions = pytest.mark.parametrize('ext', tar_extensions)
# (filename as typed by the user, unquoted filename, quoted filename as per shells.quote)
parametrize_filename = pytest.mark.parametrize('filename, unquoted, quoted', [
('foo{}', 'foo{}', 'foo{}'),
('foo\ bar{}', 'foo bar{}', "'foo bar{}'"),
('"foo bar{}"', 'foo bar{}', "'foo bar{}'")])
parametrize_script = pytest.mark.parametrize('script, fixed', [
('tar xvf {}', 'mkdir -p foo && tar xvf {} -C foo'),
('tar -xvf {}', 'mkdir -p foo && tar -xvf {} -C foo'),
('tar --extract -f {}', 'mkdir -p foo && tar --extract -f {} -C foo')])
('tar xvf {}', 'mkdir -p {dir} && tar xvf {filename} -C {dir}'),
('tar -xvf {}', 'mkdir -p {dir} && tar -xvf {filename} -C {dir}'),
('tar --extract -f {}', 'mkdir -p {dir} && tar --extract -f {filename} -C {dir}')])
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_match(tar_error, filename, script, fixed):
tar_error(filename)
assert match(Command(script=script.format(filename)))
def test_match(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
assert match(Command(script=script.format(filename.format(ext))))
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_side_effect(tar_error, filename, script, fixed):
tar_error(filename)
side_effect(Command(script=script.format(filename)), None)
assert set(os.listdir('.')) == {filename, 'd'}
def test_side_effect(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
side_effect(Command(script=script.format(filename.format(ext))), None)
assert set(os.listdir('.')) == {unquoted.format(ext), 'd'}
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_get_new_command(tar_error, filename, script, fixed):
tar_error(filename)
assert get_new_command(Command(script=script.format(filename))) == fixed.format(filename)
def test_get_new_command(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
assert (get_new_command(Command(script=script.format(filename.format(ext))))
== fixed.format(dir=quoted.format(''), filename=filename.format(ext)))

View File

@@ -1,48 +1,72 @@
# -*- coding: utf-8 -*-
import os
import pytest
import zipfile
from thefuck.rules.dirty_unzip import match, get_new_command, side_effect
from tests.utils import Command
from unicodedata import normalize
@pytest.fixture
def zip_error(tmpdir):
path = os.path.join(str(tmpdir), 'foo.zip')
def zip_error_inner(filename):
path = os.path.join(str(tmpdir), filename)
def reset(path):
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('a', '1')
archive.writestr('b', '2')
archive.writestr('c', '3')
def reset(path):
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('a', '1')
archive.writestr('b', '2')
archive.writestr('c', '3')
archive.writestr('d/e', '4')
archive.writestr('d/e', '4')
archive.extractall()
archive.extractall()
os.chdir(str(tmpdir))
reset(path)
os.chdir(str(tmpdir))
reset(path)
assert set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c', 'd'}
assert set(os.listdir('./d')) == {'e'}
dir_list = os.listdir(u'.')
if filename not in dir_list:
filename = normalize('NFD', filename)
assert set(dir_list) == {filename, 'a', 'b', 'c', 'd'}
assert set(os.listdir('./d')) == {'e'}
return zip_error_inner
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_match(zip_error, script):
@pytest.mark.parametrize('script,filename', [
(u'unzip café', u'café.zip'),
(u'unzip café.zip', u'café.zip'),
(u'unzip foo', u'foo.zip'),
(u'unzip foo.zip', u'foo.zip')])
def test_match(zip_error, script, filename):
zip_error(filename)
assert match(Command(script=script))
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_side_effect(zip_error, script):
@pytest.mark.parametrize('script,filename', [
(u'unzip café', u'café.zip'),
(u'unzip café.zip', u'café.zip'),
(u'unzip foo', u'foo.zip'),
(u'unzip foo.zip', u'foo.zip')])
def test_side_effect(zip_error, script, filename):
zip_error(filename)
side_effect(Command(script=script), None)
assert set(os.listdir('.')) == {'foo.zip', 'd'}
dir_list = os.listdir(u'.')
if filename not in set(dir_list):
filename = normalize('NFD', filename)
assert set(dir_list) == {filename, 'd'}
@pytest.mark.parametrize('script,fixed', [
('unzip foo', 'unzip foo -d foo'),
('unzip foo.zip', 'unzip foo.zip -d foo')])
def test_get_new_command(zip_error, script, fixed):
@pytest.mark.parametrize('script,fixed,filename', [
(u'unzip café', u"unzip café -d 'café'", u'café.zip'),
(u'unzip foo', u'unzip foo -d foo', u'foo.zip'),
(u"unzip foo\\ bar.zip", u"unzip foo\\ bar.zip -d 'foo bar'", u'foo.zip'),
(u"unzip 'foo bar.zip'", u"unzip 'foo bar.zip' -d 'foo bar'", u'foo.zip'),
(u'unzip foo.zip', u'unzip foo.zip -d foo', u'foo.zip')])
def test_get_new_command(zip_error, script, fixed, filename):
zip_error(filename)
assert get_new_command(Command(script=script)) == fixed

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest
import os
from thefuck.rules.fix_file import match, get_new_command
@@ -87,6 +89,20 @@ Traceback (most recent call last):
TypeError: first argument must be string or compiled pattern
"""),
(u'python café.py', u'café.py', 8, None, '',
u"""
Traceback (most recent call last):
File "café.py", line 8, in <module>
match("foo")
File "café.py", line 5, in match
m = re.search(None, command)
File "/usr/lib/python3.4/re.py", line 170, in search
return _compile(pattern, flags).search(string)
File "/usr/lib/python3.4/re.py", line 293, in _compile
raise TypeError("first argument must be string or compiled pattern")
TypeError: first argument must be string or compiled pattern
"""),
('ruby a.rb', 'a.rb', 3, None, '',
"""
a.rb:3: syntax error, unexpected keyword_end
@@ -227,7 +243,7 @@ def test_get_new_command_with_settings(mocker, monkeypatch, test, settings):
if test[3]:
assert (get_new_command(cmd) ==
'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
u'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
else:
assert (get_new_command(cmd) ==
'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))
u'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))

View File

@@ -45,8 +45,8 @@ def test_not_match(command):
@pytest.mark.parametrize('command, output', [
(Command(script='git push', stderr=git_err), 'git push --force'),
(Command(script='git push nvbn', stderr=git_err), 'git push --force nvbn'),
(Command(script='git push nvbn master', stderr=git_err), 'git push --force nvbn master')])
(Command(script='git push', stderr=git_err), 'git push --force-with-lease'),
(Command(script='git push nvbn', stderr=git_err), 'git push --force-with-lease nvbn'),
(Command(script='git push nvbn master', stderr=git_err), 'git push --force-with-lease nvbn master')])
def test_get_new_command(command, output):
assert get_new_command(command) == output

View File

@@ -0,0 +1,47 @@
import pytest
from thefuck.rules.git_two_dashes import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr(meant):
return 'error: did you mean `%s` (with two dashes ?)' % meant
@pytest.mark.parametrize('command', [
Command(script='git add -patch', stderr=stderr('--patch')),
Command(script='git checkout -patch', stderr=stderr('--patch')),
Command(script='git commit -amend', stderr=stderr('--amend')),
Command(script='git push -tags', stderr=stderr('--tags')),
Command(script='git rebase -continue', stderr=stderr('--continue'))])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command(script='git add --patch'),
Command(script='git checkout --patch'),
Command(script='git commit --amend'),
Command(script='git push --tags'),
Command(script='git rebase --continue')])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, output', [
(Command(script='git add -patch', stderr=stderr('--patch')),
'git add --patch'),
(Command(script='git checkout -patch', stderr=stderr('--patch')),
'git checkout --patch'),
(Command(script='git checkout -patch', stderr=stderr('--patch')),
'git checkout --patch'),
(Command(script='git init -bare', stderr=stderr('--bare')),
'git init --bare'),
(Command(script='git commit -amend', stderr=stderr('--amend')),
'git commit --amend'),
(Command(script='git push -tags', stderr=stderr('--tags')),
'git push --tags'),
(Command(script='git rebase -continue', stderr=stderr('--continue')),
'git rebase --continue')])
def test_get_new_command(command, output):
assert get_new_command(command) == output

View File

@@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
from thefuck.rules.grep_recursive import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('grep blah .', stderr='grep: .: Is a directory'))
assert match(Command(u'grep café .', stderr='grep: .: Is a directory'))
assert not match(Command())
def test_get_new_command():
assert get_new_command(
Command('grep blah .')) == 'grep -r blah .'
assert get_new_command(Command('grep blah .')) == 'grep -r blah .'
assert get_new_command(Command(u'grep café .')) == u'grep -r café .'

View File

@@ -1,17 +1,18 @@
from mock import Mock, patch
from mock import patch
from thefuck.rules.has_exists_script import match, get_new_command
from ..utils import Command
def test_match():
with patch('os.path.exists', return_value=True):
assert match(Mock(script='main', stderr='main: command not found'))
assert match(Mock(script='main --help',
assert match(Command(script='main', stderr='main: command not found'))
assert match(Command(script='main --help',
stderr='main: command not found'))
assert not match(Mock(script='main', stderr=''))
assert not match(Command(script='main', stderr=''))
with patch('os.path.exists', return_value=False):
assert not match(Mock(script='main', stderr='main: command not found'))
assert not match(Command(script='main', stderr='main: command not found'))
def test_get_new_command():
assert get_new_command(Mock(script='main --help')) == './main --help'
assert get_new_command(Command(script='main --help')) == './main --help'

View File

@@ -7,7 +7,7 @@ from tests.utils import Command
Command('mkdir foo/bar/baz', stderr='mkdir: foo/bar: No such file or directory'),
Command('./bin/hdfs dfs -mkdir foo/bar/baz', stderr='mkdir: `foo/bar/baz\': No such file or directory'),
Command('hdfs dfs -mkdir foo/bar/baz', stderr='mkdir: `foo/bar/baz\': No such file or directory')
])
])
def test_match(command):
assert match(command)
@@ -17,7 +17,8 @@ def test_match(command):
Command('mkdir foo/bar/baz', stderr='foo bar baz'),
Command('hdfs dfs -mkdir foo/bar/baz'),
Command('./bin/hdfs dfs -mkdir foo/bar/baz'),
Command()])
Command(),
])
def test_not_match(command):
assert not match(command)
@@ -25,7 +26,7 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('mkdir foo/bar/baz'), 'mkdir -p foo/bar/baz'),
(Command('hdfs dfs -mkdir foo/bar/baz'), 'hdfs dfs -mkdir -p foo/bar/baz'),
(Command('./bin/hdfs dfs -mkdir foo/bar/baz'), './bin/hdfs dfs -mkdir -p foo/bar/baz')])
(Command('./bin/hdfs dfs -mkdir foo/bar/baz'), './bin/hdfs dfs -mkdir -p foo/bar/baz'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -32,9 +32,9 @@ def test_match(command):
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package', 'mvn clean install']),
(Command(script='mvn -N', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn -N clean package', 'mvn -N clean install'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -32,9 +32,9 @@ def test_match(command):
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']),
(Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -6,7 +6,7 @@ from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"),
Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"),
])
])
def test_match(command):
assert match(command)
@@ -14,7 +14,7 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/', stderr=""),
Command(script='mv foo bar/foo', stderr="mv: permission denied"),
])
])
def test_not_match(command):
assert not match(command)
@@ -22,6 +22,6 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'),
(Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'),
])
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -7,25 +7,25 @@ from tests.utils import Command
Command('rm foo', stderr='rm: foo: is a directory'),
Command('rm foo', stderr='rm: foo: Is a directory'),
Command('hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory'),
Command('./bin/hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory')
])
Command('./bin/hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory'),
])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('rm foo'),
Command('rm foo'),
Command('hdfs dfs -rm foo'),
Command('./bin/hdfs dfs -rm foo'),
Command()])
Command('./bin/hdfs dfs -rm foo'),
Command(),
])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('rm foo'), 'rm -rf foo'),
(Command('hdfs dfs -rm foo'), 'hdfs dfs -rm -r foo')])
(Command('hdfs dfs -rm foo'), 'hdfs dfs -rm -r foo'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -1,6 +1,5 @@
import os
import pytest
from mock import Mock
from thefuck.rules.ssh_known_hosts import match, get_new_command,\
side_effect
from tests.utils import Command

View File

@@ -19,11 +19,13 @@ def test_match(stderr, stdout):
def test_not_match():
assert not match(Command())
assert not match(Command(script='sudo ls', stderr='Permission denied'))
@pytest.mark.parametrize('before, after', [
('ls', 'sudo ls'),
('echo a > b', 'sudo sh -c "echo a > b"'),
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"')])
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"'),
('mkdir && touch a', 'sudo sh -c "mkdir && touch a"')])
def test_get_new_command(before, after):
assert get_new_command(Command(before)) == after

View File

@@ -1,4 +1,3 @@
import pytest
from thefuck.rules.systemctl import match, get_new_command
from tests.utils import Command

View File

@@ -14,8 +14,8 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='./bin/hdfs dfs -ls', stderr=''),
Command(script='./bin/hdfs dfs -ls /foo/bar', stderr=''),
Command(script='hdfs dfs -ls -R /foo/bar', stderr=''),
Command(script='./bin/hdfs dfs -ls /foo/bar', stderr=''),
Command(script='hdfs dfs -ls -R /foo/bar', stderr=''),
Command()])
def test_not_match(command):
assert not match(command)
@@ -32,4 +32,3 @@ def test_not_match(command):
stderr='ls: Unknown command\nDid you mean -ls? This command begins with a dash.'), ['./bin/hdfs dfs -Dtest=fred -ls -R /foo/bar'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -16,8 +16,8 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='vagrant ssh', stderr=''),
Command(script='vagrant ssh jeff', stderr='The machine with the name \'jeff\' was not found configured for this Vagrant environment.'),
Command(script='vagrant ssh', stderr='A Vagrant environment or target machine is required to run this command. Run `vagrant init` to create a new Vagrant environment. Or, get an ID of a target machine from `vagrant global-status` to run this command on. A final option is to change to a directory with a Vagrantfile and to try again.'),
Command(script='vagrant ssh jeff', stderr='The machine with the name \'jeff\' was not found configured for this Vagrant environment.'),
Command(script='vagrant ssh', stderr='A Vagrant environment or target machine is required to run this command. Run `vagrant init` to create a new Vagrant environment. Or, get an ID of a target machine from `vagrant global-status` to run this command on. A final option is to change to a directory with a Vagrantfile and to try again.'),
Command()])
def test_not_match(command):
assert not match(command)
@@ -32,4 +32,3 @@ def test_not_match(command):
stderr='VM must be created before running this command. Run `vagrant up` first.'), ['vagrant up devbox && vagrant rdp devbox', 'vagrant up && vagrant rdp devbox'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -1,5 +1,4 @@
import pytest
from mock import Mock
from thefuck.specific.sudo import sudo_support
from tests.utils import Command

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest
from pathlib import PosixPath
from thefuck import corrector, conf
@@ -53,7 +55,9 @@ def test_organize_commands():
"""Ensures that the function removes duplicates and sorts commands."""
commands = [CorrectedCommand('ls'), CorrectedCommand('ls -la', priority=9000),
CorrectedCommand('ls -lh', priority=100),
CorrectedCommand(u'echo café', priority=200),
CorrectedCommand('ls -lh', priority=9999)]
assert list(organize_commands(iter(commands))) \
== [CorrectedCommand('ls'), CorrectedCommand('ls -lh', priority=100),
CorrectedCommand(u'echo café', priority=200),
CorrectedCommand('ls -la', priority=9000)]

View File

@@ -1,5 +1,4 @@
import pytest
from mock import Mock
from thefuck import logs

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck import shells
@@ -18,7 +20,7 @@ def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.__iter__.return_value = lines
.return_value.readlines.return_value = lines
return aux
@@ -35,6 +37,7 @@ class TestGeneric(object):
def test_put_to_history(self, builtins_open, shell):
assert shell.put_to_history('ls') is None
assert shell.put_to_history(u'echo café') is None
assert builtins_open.call_count == 0
def test_and_(self, shell):
@@ -48,6 +51,7 @@ class TestGeneric(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
@@ -55,6 +59,10 @@ class TestGeneric(object):
# so just ignore them:
assert list(shell.get_history()) == []
def test_split_command(self, shell):
assert shell.split_command('ls') == ['ls']
assert shell.split_command(u'echo café') == [u'echo', u'café']
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@@ -83,10 +91,13 @@ class TestBash(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, shell):
shell.put_to_history('ls')
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', 'ls\n'),
(u'echo café', 'echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, shell):
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('ls\n')
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -102,6 +113,7 @@ class TestBash(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
@@ -152,12 +164,15 @@ class TestFish(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker, shell):
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', '- cmd: ls\n when: 1430707243\n'),
(u'echo café', '- cmd: echo café\n when: 1430707243\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history('ls')
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('- cmd: ls\n when: 1430707243\n')
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
@@ -179,6 +194,12 @@ class TestFish(object):
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['- cmd: ls', ' when: 1432613911',
'- cmd: rm', ' when: 1432613916'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile')
@@ -207,12 +228,15 @@ class TestZsh(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker, shell):
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', ': 1430707243:0;ls\n'),
(u'echo café', ': 1430707243:0;echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history('ls')
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n')
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -229,6 +253,7 @@ class TestZsh(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from subprocess import PIPE
from mock import Mock
from pathlib import Path
@@ -19,6 +21,12 @@ class TestCorrectedCommand(object):
assert {CorrectedCommand('ls', None, 100),
CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')}
def test_representable(self):
assert '{}'.format(CorrectedCommand('ls', None, 100)) == \
'CorrectedCommand(script=ls, side_effect=None, priority=100)'
assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \
u'CorrectedCommand(script=echo café, side_effect=None, priority=100)'
class TestRule(object):
def test_from_path(self, mocker):
@@ -122,4 +130,3 @@ class TestCommand(object):
else:
with pytest.raises(EmptyCommand):
Command.from_raw_script(script)

View File

@@ -4,37 +4,32 @@ import pytest
from itertools import islice
from thefuck import ui
from thefuck.types import CorrectedCommand
from thefuck import const
@pytest.fixture
def patch_getch(monkeypatch):
def patch_get_key(monkeypatch):
def patch(vals):
def getch():
for val in vals:
if val == KeyboardInterrupt:
raise val
else:
yield val
getch_gen = getch()
monkeypatch.setattr('thefuck.ui.getch', lambda: next(getch_gen))
vals = iter(vals)
monkeypatch.setattr('thefuck.ui.get_key', lambda: next(vals))
return patch
def test_read_actions(patch_getch):
patch_getch([ # Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
'\x1b', '[', 'A',
# Down:
'\x1b', '[', 'B',
# Ctrl+C:
KeyboardInterrupt], )
def test_read_actions(patch_get_key):
patch_get_key([
# Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
const.KEY_UP,
# Down:
const.KEY_DOWN,
# Ctrl+C:
const.KEY_CTRL_C])
assert list(islice(ui.read_actions(), 5)) \
== [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT]
@@ -80,25 +75,25 @@ class TestSelectCommand(object):
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_getch, commands):
patch_getch(['\n'])
def test_with_confirmation(self, capsys, patch_get_key, commands):
patch_get_key(['\n'])
assert ui.select_command(iter(commands)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt])
def test_with_confirmation_abort(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_CTRL_C])
assert ui.select_command(iter(commands)) is None
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n')
def test_with_confirmation_with_side_effct(self, capsys, patch_getch,
def test_with_confirmation_with_side_effct(self, capsys, patch_get_key,
commands_with_side_effect):
patch_getch(['\n'])
assert ui.select_command(iter(commands_with_side_effect))\
patch_get_key(['\n'])
assert ui.select_command(iter(commands_with_side_effect)) \
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_select_second(self, capsys, patch_getch, commands):
patch_getch(['\x1b', '[', 'B', '\n'])
def test_with_confirmation_select_second(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_DOWN, '\n'])
assert ui.select_command(iter(commands)) == commands[1]
assert capsys.readouterr() == (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n')

View File

@@ -16,6 +16,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'no_colors': False,
'debug': False,
'priority': {},
'history_limit': None,
'alter_history': True,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@@ -23,10 +25,12 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_DEBUG': 'debug',
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
'THEFUCK_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
SETTINGS_HEADER = u"""# The Fuck settings file
#
# The rules are defined as in the example bellow:
#
@@ -71,9 +75,21 @@ class Settings(dict):
for setting in DEFAULT_SETTINGS.items():
settings_file.write(u'# {} = {}\n'.format(*setting))
def _get_user_dir_path(self):
# for backward compatibility, use `~/.thefuck` if it exists
legacy_user_dir = Path(os.path.expanduser('~/.thefuck'))
if legacy_user_dir.is_dir():
return legacy_user_dir
else:
default_xdg_config_dir = os.path.expanduser("~/.config")
xdg_config_dir = os.getenv("XDG_CONFIG_HOME", default_xdg_config_dir)
return Path(os.path.join(xdg_config_dir, 'thefuck'))
def _setup_user_dir(self):
"""Returns user config dir, create it when it doesn't exist."""
user_dir = Path(os.path.expanduser('~/.thefuck'))
user_dir = self._get_user_dir_path()
rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)
@@ -112,8 +128,11 @@ class Settings(dict):
return dict(self._priority_from_env(val))
elif attr == 'wait_command':
return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug'):
elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history'):
return val.lower() == 'true'
elif attr == 'history_limit':
return int(val)
else:
return val

14
thefuck/const.py Normal file
View File

@@ -0,0 +1,14 @@
# -*- encoding: utf-8 -*-
class _GenConst(object):
def __init__(self, name):
self._name = name
def __repr__(self):
return u'<const: {}>'.format(self._name)
KEY_UP = _GenConst('')
KEY_DOWN = _GenConst('')
KEY_CTRL_C = _GenConst('Ctrl+C')

View File

@@ -55,7 +55,7 @@ def organize_commands(corrected_commands):
key=lambda corrected_command: corrected_command.priority)
logs.debug('Corrected commands: '.format(
', '.join(str(cmd) for cmd in [first_command] + sorted_commands)))
', '.join(u'{}'.format(cmd) for cmd in [first_command] + sorted_commands)))
for command in sorted_commands:
yield command

View File

@@ -28,29 +28,29 @@ def exception(title, exc_info):
def rule_failed(rule, exc_info):
exception('Rule {}'.format(rule.name), exc_info)
exception(u'Rule {}'.format(rule.name), exc_info)
def failed(msg):
sys.stderr.write('{red}{msg}{reset}\n'.format(
sys.stderr.write(u'{red}{msg}{reset}\n'.format(
msg=msg,
red=color(colorama.Fore.RED),
reset=color(colorama.Style.RESET_ALL)))
def show_corrected_command(corrected_command):
sys.stderr.write('{bold}{script}{reset}{side_effect}\n'.format(
sys.stderr.write(u'{bold}{script}{reset}{side_effect}\n'.format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
side_effect=u' (+side effect)' if corrected_command.side_effect else u'',
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL)))
def confirm_text(corrected_command):
sys.stderr.write(
('{clear}{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
'/{red}ctrl+c{reset}]').format(
(u'{clear}{bold}{script}{reset}{side_effect} '
u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
u'/{red}ctrl+c{reset}]').format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r',

View File

@@ -1,8 +1,12 @@
# Initialize output before importing any module, that can use colorama.
from .system import init_output
init_output()
from argparse import ArgumentParser
from warnings import warn
from pprint import pformat
import sys
import colorama
from . import logs, types, shells
from .conf import settings
from .corrector import get_corrected_commands
@@ -13,7 +17,6 @@ from .ui import select_command
def fix_command():
"""Fixes previous command. Used when `thefuck` called without arguments."""
colorama.init()
settings.init()
with logs.debug_time('Total'):
logs.debug(u'Run with settings: {}'.format(pformat(settings)))
@@ -51,7 +54,6 @@ def how_to_configure_alias():
It'll be only visible when user type fuck and when alias isn't configured.
"""
colorama.init()
settings.init()
logs.how_to_configure_alias(shells.how_to_configure())

View File

@@ -0,0 +1,53 @@
import subprocess
from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app, eager, replace_command
@for_app('apt', 'apt-get', 'apt-cache')
@sudo_support
def match(command):
return 'E: Invalid operation' in command.stderr
@eager
def _parse_apt_operations(help_text_lines):
is_commands_list = False
for line in help_text_lines:
line = line.decode().strip()
if is_commands_list and line:
yield line.split()[0]
elif line.startswith('Basic commands:'):
is_commands_list = True
@eager
def _parse_apt_get_and_cache_operations(help_text_lines):
is_commands_list = False
for line in help_text_lines:
line = line.decode().strip()
if is_commands_list:
if not line:
return
yield line.split()[0]
elif line.startswith('Commands:'):
is_commands_list = True
def _get_operations(app):
proc = subprocess.Popen([app, '--help'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
lines = proc.stdout.readlines()
if app == 'apt':
return _parse_apt_operations(lines)
else:
return _parse_apt_get_and_cache_operations(lines)
@sudo_support
def get_new_command(command):
invalid_operation = command.stderr.split()[-1]
operations = _get_operations(command.script_parts[0])
return replace_command(command, invalid_operation, operations)

View File

@@ -2,14 +2,14 @@ import re
from thefuck.utils import replace_argument, for_app
@for_app('cargo')
@for_app('cargo', at_least=1)
def match(command):
return ('No such subcommand' in command.stderr
and 'Did you mean' in command.stderr)
def get_new_command(command):
broken = command.script.split()[1]
broken = command.script_parts[1]
fix = re.findall(r'Did you mean `([^`]*)`', command.stderr)[0]
return replace_argument(command.script, broken, fix)

View File

@@ -1,6 +1,7 @@
"""Attempts to spellcheck and correct failed cd commands"""
import os
import six
from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir
@@ -33,10 +34,13 @@ def get_new_command(command):
defaults to the rules of cd_mkdir.
Change sensitivity by changing MAX_ALLOWED_DIFF. Default value is 0.6
"""
dest = command.script.split()[1].split(os.sep)
dest = command.script_parts[1].split(os.sep)
if dest[-1] == '':
dest = dest[:-1]
cwd = os.getcwd()
if six.PY2:
cwd = os.getcwdu()
else:
cwd = os.getcwd()
for directory in dest:
if directory == ".":
continue
@@ -48,7 +52,7 @@ def get_new_command(command):
cwd = os.path.join(cwd, best_matches[0])
else:
return cd_mkdir.get_new_command(command)
return 'cd "{0}"'.format(cwd)
return u'cd "{0}"'.format(cwd)
enabled_by_default = True

View File

@@ -8,7 +8,8 @@ from thefuck.specific.sudo import sudo_support
@for_app('cd')
def match(command):
return (('no such file or directory' in command.stderr.lower()
or 'cd: can\'t cd to' in command.stderr.lower()))
or 'cd: can\'t cd to' in command.stderr.lower()
or 'the system cannot find the path specified.' in command.stderr.lower()))
@sudo_support

View File

@@ -1,7 +1,7 @@
from thefuck.utils import for_app
@for_app(['g++', 'clang++'])
@for_app('g++', 'clang++')
def match(command):
return ('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or

View File

@@ -4,6 +4,11 @@ from thefuck import shells
from thefuck.utils import for_app
tar_extensions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
'.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2',
'.tgz', '.tlz', '.txz', '.tz')
def _is_tar_extract(cmd):
if '--extract' in cmd:
return True
@@ -14,11 +19,7 @@ def _is_tar_extract(cmd):
def _tar_file(cmd):
tar_extensions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
'.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2',
'.tgz', '.tlz', '.txz', '.tz')
for c in cmd.split():
for c in cmd:
for ext in tar_extensions:
if c.endswith(ext):
return (c, c[0:len(c) - len(ext)])
@@ -28,16 +29,17 @@ def _tar_file(cmd):
def match(command):
return ('-C' not in command.script
and _is_tar_extract(command.script)
and _tar_file(command.script) is not None)
and _tar_file(command.script_parts) is not None)
def get_new_command(command):
dir = shells.quote(_tar_file(command.script_parts)[1])
return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
.format(dir=_tar_file(command.script)[1], cmd=command.script)
.format(dir=dir, cmd=command.script)
def side_effect(old_cmd, command):
with tarfile.TarFile(_tar_file(old_cmd.script)[0]) as archive:
with tarfile.TarFile(_tar_file(old_cmd.script_parts)[0]) as archive:
for file in archive.getnames():
try:
os.remove(file)

View File

@@ -1,11 +1,15 @@
import os
import zipfile
from thefuck.utils import for_app
from thefuck.shells import quote
def _is_bad_zip(file):
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
try:
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
except:
return False
def _zip_file(command):
@@ -13,22 +17,28 @@ def _zip_file(command):
# unzip [-flags] file[.zip] [file(s) ...] [-x file(s) ...]
# ^ ^ files to unzip from the archive
# archive to unzip
for c in command.script.split()[1:]:
for c in command.script_parts[1:]:
if not c.startswith('-'):
if c.endswith('.zip'):
return c
else:
return '{}.zip'.format(c)
return u'{}.zip'.format(c)
@for_app('unzip')
def match(command):
return ('-d' not in command.script
and _is_bad_zip(_zip_file(command)))
if '-d' in command.script:
return False
zip_file = _zip_file(command)
if zip_file:
return _is_bad_zip(zip_file)
else:
return False
def get_new_command(command):
return '{} -d {}'.format(command.script, _zip_file(command)[:-4])
return u'{} -d {}'.format(command.script, quote(_zip_file(command)[:-4]))
def side_effect(old_cmd, command):

View File

@@ -1,11 +1,13 @@
def match(command):
split_command = command.script.split()
split_command = command.script_parts
return len(split_command) >= 2 and split_command[0] == split_command[1]
return (split_command
and len(split_command) >= 2
and split_command[0] == split_command[1])
def get_new_command(command):
return command.script[command.script.find(' ')+1:]
return ' '.join(command.script_parts[1:])
# it should be rare enough to actually have to type twice the same word, so
# this rule can have a higher priority to come before things like "cd cd foo"

View File

@@ -7,38 +7,38 @@ from thefuck import shells
# order is important: only the first match is considered
patterns = (
# js, node:
'^ at {file}:{line}:{col}',
# cargo:
'^ {file}:{line}:{col}',
# python, thefuck:
'^ File "{file}", line {line}',
# awk:
'^awk: {file}:{line}:',
# git
'^fatal: bad config file line {line} in {file}',
# llc:
'^llc: {file}:{line}:{col}:',
# lua:
'^lua: {file}:{line}:',
# fish:
'^{file} \\(line {line}\\):',
# bash, sh, ssh:
'^{file}: line {line}: ',
# cargo, clang, gcc, go, pep8, rustc:
'^{file}:{line}:{col}',
# ghc, make, ruby, zsh:
'^{file}:{line}:',
# perl:
'at {file} line {line}',
)
# js, node:
'^ at {file}:{line}:{col}',
# cargo:
'^ {file}:{line}:{col}',
# python, thefuck:
'^ File "{file}", line {line}',
# awk:
'^awk: {file}:{line}:',
# git
'^fatal: bad config file line {line} in {file}',
# llc:
'^llc: {file}:{line}:{col}:',
# lua:
'^lua: {file}:{line}:',
# fish:
'^{file} \\(line {line}\\):',
# bash, sh, ssh:
'^{file}: line {line}: ',
# cargo, clang, gcc, go, pep8, rustc:
'^{file}:{line}:{col}',
# ghc, make, ruby, zsh:
'^{file}:{line}:',
# perl:
'at {file} line {line}',
)
# for the sake of readability do not use named groups above
def _make_pattern(pattern):
pattern = pattern.replace('{file}', '(?P<file>[^:\n]+)') \
.replace('{line}', '(?P<line>[0-9]+)') \
.replace('{col}', '(?P<col>[0-9]+)')
.replace('{col}', '(?P<col>[0-9]+)')
return re.compile(pattern, re.MULTILINE)
patterns = [_make_pattern(p).search for p in patterns]
@@ -58,7 +58,7 @@ def match(command):
return _search(command.stderr) or _search(command.stdout)
@default_settings({'fixlinecmd': '{editor} {file} +{line}',
@default_settings({'fixlinecmd': u'{editor} {file} +{line}',
'fixcolcmd': None})
def get_new_command(command):
m = _search(command.stderr) or _search(command.stdout)

View File

@@ -5,7 +5,8 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
# catches "git branch list" in place of "git branch"
return command.script.split()[1:] == 'branch list'.split()
return (command.script_parts
and command.script_parts[1:] == 'branch list'.split())
@git_support

View File

@@ -5,9 +5,8 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
splited_script = command.script.split()
if len(splited_script) > 1:
return (splited_script[1] == 'stash'
if command.script_parts and len(command.script_parts) > 1:
return (command.script_parts[1] == 'stash'
and 'usage:' in command.stderr)
else:
return False
@@ -26,12 +25,12 @@ stash_commands = (
@git_support
def get_new_command(command):
stash_cmd = command.script.split()[2]
stash_cmd = command.script_parts[2]
fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False)
if fixed is not None:
return replace_argument(command.script, stash_cmd, fixed)
else:
cmd = command.script.split()
cmd = command.script_parts[:]
cmd.insert(2, 'save')
return ' '.join(cmd)

View File

@@ -12,7 +12,7 @@ def match(command):
@git_support
def get_new_command(command):
return replace_argument(command.script, 'push', 'push --force')
return replace_argument(command.script, 'push', 'push --force-with-lease')
enabled_by_default = False

View File

@@ -0,0 +1,14 @@
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('error: did you mean `' in command.stderr
and '` (with two dashes ?)' in command.stderr)
@git_support
def get_new_command(command):
to = command.stderr.split('`')[1]
return replace_argument(command.script, to[1:], to)

View File

@@ -7,4 +7,4 @@ def match(command):
def get_new_command(command):
return 'grep -r {}'.format(command.script[5:])
return u'grep -r {}'.format(command.script[5:])

View File

@@ -4,7 +4,7 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return os.path.exists(command.script.split()[0]) \
return command.script_parts and os.path.exists(command.script_parts[0]) \
and 'command not found' in command.stderr

View File

@@ -3,10 +3,10 @@ from thefuck.utils import for_app
@for_app('ls')
def match(command):
return 'ls -' not in command.script
return command.script_parts and 'ls -' not in command.script
def get_new_command(command):
command = command.script.split(' ')
command = command.script_parts[:]
command[0] = 'ls -lah'
return ' '.join(command)

View File

@@ -1,5 +1,9 @@
from thefuck.utils import for_app
@for_app('man', at_least=1)
def match(command):
return command.script.strip().startswith('man ')
return True
def get_new_command(command):
@@ -8,7 +12,7 @@ def get_new_command(command):
if '2' in command.script:
return command.script.replace("2", "3")
split_cmd2 = command.script.split()
split_cmd2 = command.script_parts
split_cmd3 = split_cmd2[:]
split_cmd2.insert(1, ' 2 ')

View File

@@ -21,7 +21,7 @@ def match(command):
def get_new_command(command):
script = command.script.split(' ')
script = command.script_parts[:]
possibilities = extract_possibilities(command)
script[1] = get_closest(script[1], possibilities)
return ' '.join(script)

View File

@@ -5,16 +5,17 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
get_all_executables()))
return (command.script_parts
and 'not found' in command.stderr
and bool(get_close_matches(command.script_parts[0],
get_all_executables())))
@sudo_support
def get_new_command(command):
old_command = command.script.split(' ')[0]
old_command = command.script_parts[0]
new_cmds = get_close_matches(old_command, get_all_executables(), cutoff=0.1)
return [' '.join([new_command] + command.script.split(' ')[1:])
return [' '.join([new_command] + command.script_parts[1:])
for new_command in new_cmds]

View File

@@ -11,12 +11,14 @@ from thefuck.specific.archlinux import get_pkgfile, archlinux_env
def match(command):
return (command.script.startswith(('pacman', 'sudo pacman', 'yaourt'))
return (command.script_parts
and (command.script_parts[0] in ('pacman', 'yaourt')
or command.script_parts[0:2] == ['sudo', 'pacman'])
and 'error: target not found:' in command.stderr)
def get_new_command(command):
pgr = command.script.split()[-1]
pgr = command.script_parts[-1]
return replace_command(command, pgr, get_pkgfile(pgr))

View File

@@ -6,8 +6,8 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
toks = command.script.split()
return (len(toks) > 0
toks = command.script_parts
return (toks
and toks[0].endswith('.py')
and ('Permission denied' in command.stderr or
'command not found' in command.stderr))

View File

@@ -5,7 +5,8 @@ enabled_by_default = False
@sudo_support
def match(command):
return ({'rm', '/'}.issubset(command.script.split())
return (command.script_parts
and {'rm', '/'}.issubset(command.script_parts)
and '--no-preserve-root' not in command.script
and '--no-preserve-root' in command.stderr)

View File

@@ -1,5 +1,6 @@
import shlex
from thefuck.utils import quote, for_app
from thefuck.shells import quote
from thefuck.utils import for_app
@for_app('sed')

View File

@@ -16,10 +16,14 @@ patterns = ['permission denied',
'need root',
'only root can ',
'You don\'t have access to the history DB.',
'authentication is required']
'authentication is required',
'eDSPermissionError']
def match(command):
if command.script_parts and '&&' not in command.script_parts and command.script_parts[0] == 'sudo':
return False
for pattern in patterns:
if pattern.lower() in command.stderr.lower()\
or pattern.lower() in command.stdout.lower():
@@ -28,7 +32,9 @@ def match(command):
def get_new_command(command):
if '>' in command.script:
if '&&' in command.script:
return u'sudo sh -c "{}"'.format(" ".join([part for part in command.script_parts if part != "sudo"]))
elif '>' in command.script:
return u'sudo sh -c "{}"'.format(command.script.replace('"', '\\"'))
else:
return u'sudo {}'.format(command.script)

View File

@@ -11,9 +11,11 @@ source_layouts = [u'''йцукенгшщзхъфывапролджэячсмит
@memoize
def _get_matched_layout(command):
# don't use command.split_script here because a layout mismatch will likely
# result in a non-splitable sript as per shlex
cmd = command.script.split(' ')
for source_layout in source_layouts:
if all([ch in source_layout or ch in '-_'
for ch in command.script.split(' ')[0]]):
if all([ch in source_layout or ch in '-_' for ch in cmd[0]]):
return source_layout

View File

@@ -8,15 +8,15 @@ from thefuck.utils import for_app
@sudo_support
@for_app('systemctl')
def match(command):
# Catches 'Unknown operation 'service'.' when executing systemctl with
# Catches "Unknown operation 'service'." when executing systemctl with
# misordered arguments
cmd = command.script.split()
return ('Unknown operation \'' in command.stderr and
cmd = command.script_parts
return (cmd and 'Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3)
@sudo_support
def get_new_command(command):
cmd = command.script.split()
cmd = command.script_parts
cmd[-1], cmd[-2] = cmd[-2], cmd[-1]
return ' '.join(cmd)

View File

@@ -13,6 +13,6 @@ def get_new_command(command):
command.stderr)
old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
suggestions = [c.strip() for c in cmd.group(2).split(',')]
return replace_command(command, old_cmd, suggestions)

View File

@@ -1,6 +1,6 @@
import re
from thefuck import shells
from thefuck.utils import for_app
from thefuck.shells import and_
@for_app('touch')
@@ -10,4 +10,4 @@ def match(command):
def get_new_command(command):
path = re.findall(r"touch: cannot touch '(.+)/.+':", command.stderr)[0]
return and_(u'mkdir -p {}'.format(path), command.script)
return shells.and_(u'mkdir -p {}'.format(path), command.script)

View File

@@ -1,6 +1,7 @@
import re
from thefuck.utils import replace_command
def match(command):
return (re.search(r"([^:]*): Unknown command.*", command.stderr) != None
and re.search(r"Did you mean ([^?]*)?", command.stderr) != None)
@@ -10,4 +11,3 @@ def get_new_command(command):
broken_cmd = re.findall(r"([^:]*): Unknown command.*", command.stderr)[0]
matched = re.findall(r"Did you mean ([^?]*)?", command.stderr)
return replace_command(command, broken_cmd, matched)

View File

@@ -8,13 +8,13 @@ def match(command):
def get_new_command(command):
cmds = command.script.split(' ')
cmds = command.script_parts
machine = None
if len(cmds) >= 3:
machine = cmds[2]
startAllInstances = shells.and_("vagrant up", command.script)
if machine is None:
if machine is None:
return startAllInstances
else:
return [ shells.and_("vagrant up " + machine, command.script), startAllInstances]
return [shells.and_("vagrant up " + machine, command.script), startAllInstances]

View File

@@ -1,7 +1,9 @@
# -*- encoding: utf-8 -*-
from six.moves.urllib.parse import urlparse
from thefuck.utils import for_app
@for_app('whois', at_least=1)
def match(command):
"""
What the `whois` command returns depends on the 'Whois server' it contacted
@@ -19,11 +21,11 @@ def match(command):
- www.google.fr → subdomain: www, domain: 'google.fr';
- google.co.uk → subdomain: None, domain; 'google.co.uk'.
"""
return 'whois ' in command.script.strip()
return True
def get_new_command(command):
url = command.script.split()[1]
url = command.script_parts[1]
if '/' in command.script:
return 'whois ' + urlparse(url).netloc

View File

@@ -1,6 +1,6 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and `get_aliases`
methods.
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and
`get_aliases` methods.
"""
from collections import defaultdict
@@ -9,11 +9,15 @@ from subprocess import Popen, PIPE
from time import time
import io
import os
import shlex
import sys
import six
from .utils import DEVNULL, memoize, cache
from .conf import settings
from . import logs
class Generic(object):
def get_aliases(self):
return {}
@@ -34,7 +38,8 @@ class Generic(object):
return command_script
def app_alias(self, fuck):
return "alias {0}='TF_ALIAS={0} eval $(thefuck $(fc -ln -1))'".format(fuck)
return "alias {0}='TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
@@ -47,25 +52,26 @@ class Generic(object):
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script))
def _script_from_history(self, line):
"""Returns prepared history line.
Should return a blank line if history line is corrupted or empty.
"""
return ''
entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)
def get_history(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history:
for line in history:
prepared = self._script_from_history(line)\
.strip()
encoding='utf-8', errors='ignore') as history_file:
lines = history_file.readlines()
if settings.history_limit:
lines = lines[-settings.history_limit:]
for line in lines:
prepared = self._script_from_history(line) \
.strip()
if prepared:
yield prepared
@@ -75,10 +81,30 @@ class Generic(object):
def how_to_configure(self):
return
def split_command(self, command):
"""Split the command using shell-like syntax."""
if six.PY2:
return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))]
return shlex.split(command)
def quote(self, s):
"""Return a shell-escaped version of the string s."""
if six.PY2:
from pipes import quote
else:
from shlex import quote
return quote(s)
def _script_from_history(self, line):
return line
class Bash(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0} alias {0}='eval $(thefuck $(fc -ln -1));" \
return "TF_ALIAS={0} alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1));" \
" history -r'".format(fuck)
def _parse_alias(self, alias):
@@ -92,9 +118,9 @@ class Bash(Generic):
def get_aliases(self):
proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -103,9 +129,6 @@ class Bash(Generic):
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
def _script_from_history(self, line):
return line
def how_to_configure(self):
if os.path.join(os.path.expanduser('~'), '.bashrc'):
config = '~/.bashrc'
@@ -117,7 +140,6 @@ class Bash(Generic):
class Fish(Generic):
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
@@ -126,19 +148,20 @@ class Fish(Generic):
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self, fuck):
return ("set TF_ALIAS {0}\n"
"function {0} -d 'Correct your previous console command'\n"
" set -l exit_code $status\n"
" set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_command $history[1]\n"
" thefuck $fucked_up_command > $eval_script\n"
" . $eval_script\n"
" /bin/rm $eval_script\n"
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_command\n"
" end\n"
"end").format(fuck)
return ('function {0} -d "Correct your previous console command"\n'
' set -l exit_code $status\n'
' set -l fucked_up_command $history[1]\n'
' env TF_ALIAS={0} PYTHONIOENCODING=utf-8'
' thefuck $fucked_up_command | read -l unfucked_command\n'
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n'
' if test $exit_code -ne 0\n'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' return 0\n'
' end\n'
' end\n'
'end').format(fuck)
@memoize
def get_aliases(self):
@@ -165,6 +188,12 @@ class Fish(Generic):
def _get_history_line(self, command_script):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def _script_from_history(self, line):
if '- cmd: ' in line:
return line.split('- cmd: ', 1)[1]
else:
return ''
def and_(self, *commands):
return u'; and '.join(commands)
@@ -175,7 +204,8 @@ class Fish(Generic):
class Zsh(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0}" \
" alias {0}='eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'".format(fuck)
def _parse_alias(self, alias):
@@ -189,9 +219,9 @@ class Zsh(Generic):
def get_aliases(self):
proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -224,9 +254,9 @@ class Tcsh(Generic):
def get_aliases(self):
proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -273,7 +303,10 @@ def thefuck_alias():
def put_to_history(command):
return _get_shell().put_to_history(command)
try:
return _get_shell().put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
def and_(*commands):
@@ -284,9 +317,18 @@ def get_aliases():
return list(_get_shell().get_aliases().keys())
def split_command(command):
return _get_shell().split_command(command)
def quote(s):
return _get_shell().quote(s)
@memoize
def get_history():
return list(_get_shell().get_history())
def how_to_configure():
return _get_shell().how_to_configure()

View File

@@ -1,8 +1,7 @@
import re
from shlex import split
from decorator import decorator
from ..types import Command
from ..utils import quote, is_app
from ..utils import is_app
from ..shells import quote, split_command
@decorator
@@ -24,7 +23,7 @@ def git_support(fn, command):
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split(search.group(2))))
expansion = ' '.join(map(quote, split_command(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = command.update(script=new_script)

View File

@@ -1,6 +1,5 @@
import six
from decorator import decorator
from ..types import Command
@decorator

View File

@@ -0,0 +1,7 @@
import sys
if sys.platform == 'win32':
from .win32 import *
else:
from .unix import *

35
thefuck/system/unix.py Normal file
View File

@@ -0,0 +1,35 @@
import sys
import tty
import termios
import colorama
from .. import const
init_output = colorama.init
def getch():
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def get_key():
ch = getch()
if ch == '\x03':
return const.KEY_CTRL_C
elif ch == '\x1b':
next_ch = getch()
if next_ch == '[':
last_ch = getch()
if last_ch == 'A':
return const.KEY_UP
elif last_ch == 'B':
return const.KEY_DOWN
return ch

25
thefuck/system/win32.py Normal file
View File

@@ -0,0 +1,25 @@
import sys
import msvcrt
import win_unicode_console
from .. import const
def init_output():
import colorama
win_unicode_console.enable()
colorama.init()
def get_key():
ch = msvcrt.getch()
if ch in (b'\x00', b'\xe0'): # arrow or function key prefix?
ch = msvcrt.getch() # second call returns the actual key code
if ch == b'\x03':
raise const.KEY_CTRL_C
if ch == b'H':
return const.KEY_UP
if ch == b'P':
return const.KEY_DOWN
return ch.decode(sys.stdout.encoding)

View File

@@ -1,13 +1,13 @@
from imp import load_source
import os
from subprocess import Popen, PIPE
import os
import sys
from psutil import Process, TimeoutExpired
import six
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .utils import compatibility_call
from .exceptions import EmptyCommand
from psutil import Process, TimeoutExpired
from . import logs, shells
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand
from .utils import compatibility_call
class Command(object):
@@ -25,16 +25,27 @@ class Command(object):
self.stdout = stdout
self.stderr = stderr
@property
def script_parts(self):
if not hasattr(self, '_script_parts'):
try:
self._script_parts = shells.split_command(self.script)
except Exception:
logs.debug(u"Can't split command script {} because:\n {}".format(
self, sys.exc_info()))
self._script_parts = None
return self._script_parts
def __eq__(self, other):
if isinstance(other, Command):
return (self.script, self.stdout, self.stderr) \
== (other.script, other.stdout, other.stderr)
== (other.script, other.stdout, other.stderr)
else:
return False
def __repr__(self):
return 'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
return u'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
def update(self, **kwargs):
"""Returns new command with replaced fields.
@@ -155,9 +166,9 @@ class Rule(object):
return 'Rule(name={}, match={}, get_new_command={}, ' \
'enabled_by_default={}, side_effect={}, ' \
'priority={}, requires_output)'.format(
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
@classmethod
def from_path(cls, path):
@@ -256,9 +267,9 @@ class CorrectedCommand(object):
return (self.script, self.side_effect).__hash__()
def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
def run(self, old_cmd):
"""Runs command from rule for passed command.
@@ -267,5 +278,7 @@ class CorrectedCommand(object):
"""
if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
shells.put_to_history(self.script)
if settings.alter_history:
shells.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
print(self.script)

View File

@@ -3,25 +3,8 @@
import sys
from .conf import settings
from .exceptions import NoRuleMatched
from . import logs
try:
from msvcrt import getch
except ImportError:
def getch():
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == '\x03': # For compatibility with msvcrt.getch
raise KeyboardInterrupt
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
from .system import get_key
from . import logs, const
SELECT = 0
ABORT = 1
@@ -31,23 +14,17 @@ NEXT = 3
def read_actions():
"""Yields actions for pressed keys."""
buffer = []
while True:
try:
ch = getch()
except KeyboardInterrupt: # Ctrl+C
yield ABORT
key = get_key()
if ch in ('\n', '\r'): # Enter
yield SELECT
buffer.append(ch)
buffer = buffer[-3:]
if buffer == ['\x1b', '[', 'A'] or ch == 'k': # ↑
if key in (const.KEY_UP, 'k'):
yield PREVIOUS
elif buffer == ['\x1b', '[', 'B'] or ch == 'j': # ↓
elif key in (const.KEY_DOWN, 'j'):
yield NEXT
elif key == const.KEY_CTRL_C:
yield ABORT
elif key in ('\n', '\r'):
yield SELECT
class CommandSelector(object):

View File

@@ -1,27 +1,20 @@
from difflib import get_close_matches
from functools import wraps
import shelve
from warnings import warn
from decorator import decorator
from contextlib import closing
import dbm
import os
import pickle
import re
from inspect import getargspec
from pathlib import Path
import pkg_resources
import six
import re
import shelve
from .conf import settings
from contextlib import closing
from decorator import decorator
from difflib import get_close_matches
from functools import wraps
from inspect import getargspec
from pathlib import Path
from warnings import warn
DEVNULL = open(os.devnull, 'w')
if six.PY2:
from pipes import quote
else:
from shlex import quote
def memoize(fn):
"""Caches previous calls to the function."""
@@ -29,11 +22,16 @@ def memoize(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
if not memoize.disabled:
key = pickle.dumps((args, kwargs))
if key not in memo:
memo[key] = fn(*args, **kwargs)
value = memo[key]
else:
# Memoize is disabled, call the function
value = fn(*args, **kwargs)
return memo[key]
return value
return wrapper
memoize.disabled = False
@@ -112,7 +110,7 @@ def get_all_executables():
def replace_argument(script, from_, to):
"""Replaces command line argument."""
replaced_in_the_end = re.sub(u' {}$'.format(from_), u' {}'.format(to),
replaced_in_the_end = re.sub(u' {}$'.format(re.escape(from_)), u' {}'.format(to),
script, count=1)
if replaced_in_the_end != script:
return replaced_in_the_end
@@ -144,19 +142,23 @@ def replace_command(command, broken, matched):
@memoize
def is_app(command, *app_names):
def is_app(command, *app_names, **kwargs):
"""Returns `True` if command is call to one of passed app names."""
for name in app_names:
if command.script == name \
or command.script.startswith(u'{} '.format(name)):
return True
at_least = kwargs.pop('at_least', 0)
if kwargs:
raise TypeError("got an unexpected keyword argument '{}'".format(kwargs.keys()))
if command.script_parts is not None and len(command.script_parts) > at_least:
return command.script_parts[0] in app_names
return False
def for_app(*app_names):
def for_app(*app_names, **kwargs):
"""Specifies that matching script is for on of app names."""
def _for_app(fn, command):
if is_app(command, *app_names):
if is_app(command, *app_names, **kwargs):
return fn(command)
else:
return False
@@ -180,25 +182,51 @@ def cache(*depends_on):
except OSError:
return '0'
def _get_cache_path():
default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
# Ensure the cache_path exists, Python 2 does not have the exist_ok
# parameter
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_path
@decorator
def _cache(fn, *args, **kwargs):
if cache.disabled:
return fn(*args, **kwargs)
cache_path = settings.user_dir.joinpath('.thefuck-cache').as_posix()
# A bit obscure, but simplest way to generate unique key for
# functions and methods in python 2 and 3:
key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0])
etag = '.'.join(_get_mtime(name) for name in depends_on)
cache_path = _get_cache_path()
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
try:
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
except dbm.error:
# Caused when going from Python 2 to Python 3
warn("Removing possibly out-dated cache")
os.remove(cache_path)
with closing(shelve.open(cache_path)) as db:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
return _cache
cache.disabled = False

View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27,py33,py34
envlist = py27,py33,py34,py35
[testenv]
deps = -rrequirements.txt