1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-02 16:12:08 +00:00

Compare commits

...

57 Commits
3.26 ... 3.29

Author SHA1 Message Date
Vladimir Iakovlev
59e1f7b122 Bump to 3.29 2019-05-27 18:29:04 +02:00
Pablo Aguiar
ff2944086d #N/A: Improve how version is fetched for all shells (#920) 2019-05-27 18:24:55 +02:00
Pablo Aguiar
ba949f7fd9 #N/A: Add pyenv_no_such_command rule (#919) 2019-05-27 18:23:45 +02:00
Pablo Aguiar
5efcf1019f #N/A: Improve support to Windows in no_command rule (#918)
Windows “not found” message is quite different from POSIX systems.
2019-05-27 18:23:06 +02:00
Ryan Delaney
70a13406f0 Fix a couple small shellcheck errors (#915)
* Fix shellcheck SC2046

Further reading: https://github.com/koalaman/shellcheck/wiki/Sc2046

* Fix shellcheck 2068

Further reading: https://github.com/koalaman/shellcheck/wiki/Sc2068

* Fix syntax error from bad quoting

I also used a docstring here because the escaping makes it harder for
humans to parse
2019-05-22 20:22:09 +02:00
Jesus Cuesta
201c01fc74 Adding yay AUR manager to Arch Linux's commands since yaourt is unmaintained and has some security issues. (#907) 2019-05-21 20:49:04 +02:00
Pablo Aguiar
78ef9eec88 #902: Use os.pathsep to split PATH env var (#917)
Fix #902
2019-05-21 20:47:47 +02:00
Pablo Aguiar
40ab4eb62d #899: Support -y/--yeah command line args in Fish Shell (#900) 2019-04-24 18:17:52 +02:00
Nick "darkfiberiru" Wolff
55cb3546df Pkg should be recommend on freebsd to install (#905) 2019-04-24 18:17:01 +02:00
Inga Feick
82902fb50d Add rule for pip_install permission fix (#895)
* Add rule for pip_install permission fix

* Fix whitespace

* Switch quotation to single

* remove 2nd else

* E261 indent comment
2019-04-24 18:15:38 +02:00
Inga Feick
828ae537da Docker login (#894)
* Add docker_login rule

* Add docker_login rule

* Whitespace fix

* Fix typo in test case

* Fix typo in test case

* Add test cases
2019-04-04 00:01:14 +02:00
Aiden Song
1208faaabb #N/A: Add rule for git commit that reverts previous commit (#886) 2019-02-25 23:24:16 +01:00
Pablo Santiago Blum de Aguiar
2d81166213 #N/A: Return an ordered list from set of overridden aliases
This way it's ensured that whatever is used as cache key is always
ordered. Sets are unordered collections.
2019-01-17 00:29:22 +01:00
Vladimir Iakovlev
8093f7cab8 Squashed commit of the following:
commit b853385ea9b9409a29a30c7af4d47c9a500cd287
Author: Vladimir Iakovlev <nvbn.rm@gmail.com>
Date:   Tue Jan 15 00:54:01 2019 +0100

    #864: Make the solution for Greek a bit more extensible

commit 073ebceb594ad24972f7765b1f608de44c1cebf2
Merge: b946b7d 141462a
Author: Vladimir Iakovlev <nvbn.rm@gmail.com>
Date:   Tue Jan 15 00:46:09 2019 +0100

    Merge branch 'master' of git://github.com/RealOgre/thefuck into RealOgre-master

commit 141462a6fb
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 16:47:43 2018 +0200

    Update switch_lang.py

commit 1f792853f2
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 16:39:04 2018 +0200

    Update switch_lang.py

commit e7dede53a1
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 15:24:10 2018 +0200

    Update switch_lang.py

commit 4a0a973e62
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 15:04:44 2018 +0200

    Update switch_lang.py

commit 80d6b8da4c
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 14:25:15 2018 +0200

    Update switch_lang.py

commit 66b13c53b3
Author: RealOrge <45096491+RealOrge@users.noreply.github.com>
Date:   Thu Dec 13 11:44:48 2018 +0200

    Update switch_lang.py
2019-01-15 00:54:48 +01:00
yangkyeongmo
b946b7d319 Comment correction on ui.py (#874) 2019-01-15 00:36:05 +01:00
Vladimir Iakovlev
9354a977dd #N/A: Fix tests with pytest 4 2019-01-15 00:35:28 +01:00
Mickaël Schoentgen
1eb4ccbcc9 Fix 2 DeprecationWarning: invlid escape sequence (#872)
Signed-off-by: Mickaël Schoentgen <contact@tiger-222.fr>
2019-01-06 15:44:09 +01:00
Pablo Santiago Blum de Aguiar
ce5feaebf7 #869: Use fish --version instead of an interactive shell for info()
This prevents initialisation and consequentially a recursive loop.

Fix #869
Ref oh-my-fish/plugin-thefuck#11
2019-01-04 20:54:03 +01:00
Fábio Santos
ac343fb1bd #868: Point out you can use linuxbrew in README
* Point out you can use linuxbrew

* Change text and add link to linuxbrew

* Change brew related URLs to HTTPS
2019-01-04 20:35:27 +01:00
Chris De Pasquale
7bc619385b Fixed incorrect ordering of for_app and sudo_support causing apt_invalid_operation and dnf_no_such_command rules to fail (#861) 2018-12-11 01:01:17 +01:00
Vladimir Iakovlev
d86dd5f179 #N/A: Clear dist/ before uploading releases 2018-11-29 23:57:07 +01:00
Vladimir Iakovlev
8c1591fbe3 Bump to 3.28 2018-11-29 23:43:39 +01:00
kozar
dfd31872a9 #855 - Support Ukrainian layout; Fix matching of similar layouts (#856)
*  #855 - Support Ukrainian layout; Fix matching of similar layouts

* Fix splitting of command line
2018-11-21 19:56:49 +01:00
Vladimir Iakovlev
1eaead4f70 #N/A: Remove performance tests as they are meaningless with the current implementation 2018-11-21 19:44:07 +01:00
Vladimir Iakovlev
5b3350b2dd #N/A: Fix tests with py.test 4 2018-11-21 19:43:01 +01:00
Vladimir Iakovlev
81b05b9f88 #N/A: Fix osx travis-ci build 2018-11-21 19:31:59 +01:00
Simon Eisenmann
b5436a2c47 Remove zsh instant mode log with -f (#854)
In some setups, rm might default to interactive promt. This change adds
the -f parameter to force remove the instant mode log on exit to avoid
an interactive prompt.

```
 ~ 
rm: remove regular file
'/tmp/user/1000/thefuck-script-log-bbb81260140c4b3fa18bf2097f15bd77'?
```
2018-11-02 20:00:56 +01:00
Přemek Vyhnal
b08aec02f5 update install guide for Linux Mint (#852)
on Mint I had to install python3-setuptools package too
2018-10-30 21:49:42 +01:00
Pablo Aguiar
e6be00a63b Comply to new flake8 3.6 (#853)
* #N/A: Ignore W504 line break after binary operator

W504 is now part of flake8 current version 3.6

* #N/A: Fix invalid escape sequences

* #N/A: Remove conflicting path before installing gcc with brew
2018-10-30 20:56:55 +01:00
Vladimir Iakovlev
d226b8f258 #835: Make cache failure non-fatal 2018-10-18 00:35:18 +02:00
Vladimir Iakovlev
f06ebbf2ae #N/A: Use twine for uploading new releases 2018-10-16 21:08:08 +02:00
Nallagatla Manikanta
3e522ba787 Add the pwsh support for thefuck (#844) 2018-10-09 23:20:48 +02:00
Pablo Aguiar
25142f81f8 Some improvements (#846)
* #833: do not require sudo on TravisCI

* #N/A: Add Python dev releases to TravisCI pipeline

Inspired by Brett Cannon's advise [1].

    1: https://snarky.ca/how-to-use-your-project-travis-to-help-test-python-itself/

* #837: try and kill proc and its children

* #N/A: show shell information on `thefuck --version`

* #N/A: omit default arguments to get_close_matches

* #842: add settings var to control number of close matches

* #N/A: remove `n` from the list of `get_closest`'s args
2018-10-08 22:32:30 +02:00
Rafał Zawadzki
5fd4f74701 Added back-ticks for the consistency (#845) 2018-10-08 22:20:17 +02:00
Waldir Pimenta
a0286b402a ISSUE_TEMPLATE.md: sync format of fill-in fields (#841)
Some of the "FILL THIS IN" were wrapped with html comment markup, but most of them weren't. This change makes them all use the same format.
2018-10-02 20:46:58 +02:00
Mateusz Mikuła
bb41f5c4e5 Add snapcraft.yaml (#836) 2018-10-02 20:46:13 +02:00
James Turnbull
926e9ef963 Added a rule to match az binary sub-command misses (#834) 2018-08-16 00:22:24 +02:00
Eugene Duboviy
9d46291944 Add Python 3.7 version support (#833) 2018-08-14 00:22:53 +02:00
Pablo Aguiar
cc6d90963e #367: Support BSD style output in touch rule (#830)
On a Mac, also on NetBSD or OpenBSD, `touch` errs differently:

```
$ uname; touch a/b/c
Darwin
touch: a/b/c: No such file or directory
```

That gets matched by the rule but not fixed by it. Thus the regex
pattern is now a bit more tolerant.
2018-07-29 18:18:42 +02:00
Vladimir Iakovlev
4e755e4799 #827: Make cat_dir rule safer 2018-07-11 23:57:41 +02:00
Scott Colby
1dfd6373ee Stop parsing language-variable cat output to make cat_dir more reliable. (#827)
* Stop parsing language-variable cat output to make cat_dir more reliable.

* Add missing semicolon in readme
2018-07-11 23:47:06 +02:00
Scott Colby
fe0785bc42 Create cat_dir rule for replacing cat with ls (#823)
* Create `cat_dir` rule for replacing `cat` with `ls` when you try to run `cat` on a directory.

* Changed to string methods in response to feedback.

Added a test to make sure lines like 'cat cat' don't become 'ls ls'.

Added trailing '\n's to test cases.
2018-07-10 00:51:42 +02:00
Glen Yu
142ef6e66c added --yeah as an alterative arg to -y and --yes; updated README.md (#822) 2018-07-10 00:50:11 +02:00
Mas0s
59745942b5 added notice to README.md (#821)
zsh's autocorrect function interferes with thefuck, that is now mentioned in README
2018-07-10 00:49:20 +02:00
Chris Mendis
692ee53a33 Avoid masking shell return values in Zsh.app_alias (#820)
I discovered that a common shell script issue (https://github.com/koalaman/shellcheck/wiki/SC2155) is present in the script returned by `Zsh.app_alias()`, amongst other `app_alias` methods in thefuck.

In the case of the zsh `app_alias()` script, this common issue is causing the script to error on some versions of Mac OS X. This is  reported in #718.

Unmasking the value of `TF_SHELL_ALIASES` fixes the errors in Mac OS X, but I also unmasked `TF_HISTORY` for consistency.
2018-07-10 00:48:56 +02:00
Matthieu Guilbert
534782414f git_push: Handle command containing force argument (#818) 2018-07-10 00:48:08 +02:00
Matt Kotsenas
a6bb41e802 Fix Win32 get_key (#819)
PR #711 moved the arrow and cancel key codes to `const.py`. However, the
move also changed the codes from byte arrays to strings, which broke the
use of `msvcrt.getch()` for Windows.

The fix is to use `msvcrt.getwch()` so the key is a Unicode character,
matching the Unix implementation.
2018-07-10 00:46:57 +02:00
Vladimir Iakovlev
86efc6a252 Bump to 3.27 2018-05-22 19:26:55 +02:00
Iulian Onofrei
89207d6d7c Add brew_reinstall rule (#816)
Replaces install with reinstall when a package is already installed.
2018-05-22 19:25:05 +02:00
afwilkin
f6e50bef82 fixed powershell coloring (#805) 2018-05-22 19:03:52 +02:00
Vladimir Iakovlev
81042514c8 Squashed commit of the following:
commit 6919161e77a39b9bd59ca54eac44b956cd3ae1dc
Author: Vladimir Iakovlev <nvbn.rm@gmail.com>
Date:   Tue May 22 19:01:33 2018 +0200

    #810: Fix code style

commit ebbb31a3227ce32ba5288e96c0c16a3d334c45d6
Merge: 2df1a5a a2799ad
Author: Vladimir Iakovlev <nvbn.rm@gmail.com>
Date:   Tue May 22 18:59:56 2018 +0200

    Merge branch 'feature/long-form-help' of https://github.com/jakewarren/thefuck into jakewarren-feature/long-form-help

commit a2799ad098
Author: Jake Warren <jakewarren@users.noreply.github.com>
Date:   Mon May 7 14:12:57 2018 -0500

    Add new `long_form_help` rule
2018-05-22 19:01:51 +02:00
Vladimir Iakovlev
2df1a5a45b #N/A: Fix formatting 2018-05-14 22:44:10 +02:00
Vladimir Iakovlev
72e88d6ba3 #N/A: Add basic shell logger support 2018-05-14 22:16:33 +02:00
Pablo Aguiar
8db3cf6048 Support aliases with equal sign (#808)
* #N/A: Remove `pip` from requirements.txt

* #807: Expect aliases declared with equal sign too

This fixes #807
2018-05-13 15:29:33 +02:00
Iulian Onofrei
68949a5922 Fix spelling (#814) 2018-05-13 15:28:39 +02:00
Pablo Aguiar
216d82b464 #N/A: Remove pip from requirements.txt (#813) 2018-05-13 15:28:00 +02:00
Evan Pratten
97f2d743b3 Update README.md 2018-05-11 11:36:24 +02:00
78 changed files with 1035 additions and 189 deletions

View File

@@ -6,17 +6,14 @@ update The Fuck and see if the bug is still there. -->
if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with
the following basic information: --> the following basic information: -->
The output of `thefuck --version` (something like `The Fuck 3.1 using Python 3.5.0`): The output of `thefuck --version` (something like `The Fuck 3.1 using Python
3.5.0 and Bash 4.4.12(1)-release`):
FILL THIS IN
Your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.):
FILL THIS IN FILL THIS IN
Your system (Debian 7, ArchLinux, Windows, etc.): Your system (Debian 7, ArchLinux, Windows, etc.):
<!-- FILL THIS IN --> FILL THIS IN
How to reproduce the bug: How to reproduce the bug:
@@ -32,6 +29,6 @@ If the bug only appears with a specific application, the output of that applicat
Anything else you think is relevant: Anything else you think is relevant:
<!-- FILL THIS IN --> FILL THIS IN
<!-- It's only with enough information that we can do something to fix the problem. --> <!-- It's only with enough information that we can do something to fix the problem. -->

View File

@@ -2,6 +2,21 @@ language: python
sudo: false sudo: false
matrix: matrix:
include: include:
- os: linux
dist: xenial
python: "nightly"
- os: linux
dist: xenial
python: "3.8-dev"
- os: linux
dist: xenial
python: "3.7-dev"
- os: linux
dist: xenial
python: "3.7"
- os: linux
dist: trusty
python: "3.6-dev"
- os: linux - os: linux
dist: trusty dist: trusty
python: "3.6" python: "3.6"
@@ -16,6 +31,11 @@ matrix:
python: "2.7" python: "2.7"
- os: osx - os: osx
language: generic language: generic
allow_failures:
- python: nightly
- python: 3.8-dev
- python: 3.7-dev
- python: 3.6-dev
services: services:
- docker - docker
addons: addons:
@@ -24,7 +44,8 @@ addons:
- python-commandnotfound - python-commandnotfound
- python3-commandnotfound - python3-commandnotfound
before_install: before_install:
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update ; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then rm -rf /usr/local/include/c++; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew upgrade python; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew upgrade python; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then pip3 install virtualenv; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then pip3 install virtualenv; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p python3; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p python3; fi
@@ -39,7 +60,7 @@ script:
- flake8 - flake8
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1} - export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys tests" - export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys tests"
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi - if [[ $TRAVIS_PYTHON_VERSION == 3.7 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi
- if [[ $TRAVIS_PYTHON_VERSION != 3.6 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi - if [[ $TRAVIS_PYTHON_VERSION != 3.7 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi
after_success: after_success:
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi - if [[ $TRAVIS_PYTHON_VERSION == 3.7 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi

View File

@@ -99,23 +99,27 @@ Reading package lists... Done
## Installation ## Installation
On OS X, you can install *The Fuck* via [Homebrew][homebrew]: On OS X, you can install *The Fuck* via [Homebrew][homebrew] (or via [Linuxbrew][linuxbrew] on Linux):
```bash ```bash
brew install thefuck brew install thefuck
``` ```
On Ubuntu, install *The Fuck* with the following commands: On Ubuntu / Mint, install *The Fuck* with the following commands:
```bash ```bash
sudo apt update sudo apt update
sudo apt install python3-dev python3-pip sudo apt install python3-dev python3-pip python3-setuptools
sudo pip3 install thefuck sudo pip3 install thefuck
``` ```
On FreeBSD, install *The Fuck* with the following commands: On FreeBSD, install *The Fuck* with the following commands:
```bash ```bash
sudo portsnap fetch update pkg install thefuck
cd /usr/ports/misc/thefuck && sudo make install clean ```
On ChromeOS, install *The Fuck* using [chromebrew](https://github.com/skycocker/chromebrew) with the following command:
```bash
crew install thefuck
``` ```
On other systems, install *The Fuck* by using `pip`: On other systems, install *The Fuck* by using `pip`:
@@ -141,10 +145,10 @@ eval $(thefuck --alias FUCK)
Changes are only available in a new shell session. To make changes immediately Changes are only available in a new shell session. To make changes immediately
available, run `source ~/.bashrc` (or your shell config file like `.zshrc`). available, run `source ~/.bashrc` (or your shell config file like `.zshrc`).
To run fixed commands without confirmation, use the `-y` option: To run fixed commands without confirmation, use the `--yeah` option (or just `-y` for short):
```bash ```bash
fuck -y fuck --yeah
``` ```
To fix commands recursively until succeeding, use the `-r` option: To fix commands recursively until succeeding, use the `-r` option:
@@ -170,8 +174,10 @@ following rules are enabled by default:
* `adb_unknown_command` &ndash; fixes misspelled commands like `adb logcta`; * `adb_unknown_command` &ndash; fixes misspelled commands like `adb logcta`;
* `ag_literal` &ndash; adds `-Q` to `ag` when suggested; * `ag_literal` &ndash; adds `-Q` to `ag` when suggested;
* `aws_cli` &ndash; fixes misspelled commands like `aws dynamdb scan`; * `aws_cli` &ndash; fixes misspelled commands like `aws dynamdb scan`;
* `az_cli` &ndash; fixes misspelled commands like `az providers`;
* `cargo` &ndash; runs `cargo build` instead of `cargo`; * `cargo` &ndash; runs `cargo build` instead of `cargo`;
* `cargo_no_command` &ndash; fixes wrongs commands like `cargo buid`; * `cargo_no_command` &ndash; fixes wrongs commands like `cargo buid`;
* `cat_dir` &ndash; replaces `cat` with `ls` when you try to `cat` a directory;
* `cd_correction` &ndash; spellchecks and correct failed cd commands; * `cd_correction` &ndash; spellchecks and correct failed cd commands;
* `cd_mkdir` &ndash; creates directories before cd'ing into them; * `cd_mkdir` &ndash; creates directories before cd'ing into them;
* `cd_parent` &ndash; changes `cd..` to `cd ..`; * `cd_parent` &ndash; changes `cd..` to `cd ..`;
@@ -183,6 +189,7 @@ following rules are enabled by default:
* `dirty_unzip` &ndash; fixes `unzip` command that unzipped in the current directory; * `dirty_unzip` &ndash; fixes `unzip` command that unzipped in the current directory;
* `django_south_ghost` &ndash; adds `--delete-ghost-migrations` to failed because ghosts django south migration; * `django_south_ghost` &ndash; adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` &ndash; adds `--merge` to inconsistent django south migration; * `django_south_merge` &ndash; adds `--merge` to inconsistent django south migration;
* `docker_login` &ndash; executes a `docker login` and repeats the previous command;
* `docker_not_command` &ndash; fixes wrong docker commands like `docker tags`; * `docker_not_command` &ndash; fixes wrong docker commands like `docker tags`;
* `dry` &ndash; fixes repetitions like `git git push`; * `dry` &ndash; fixes repetitions like `git git push`;
* `fab_command_not_found` &ndash; fix misspelled fabric commands; * `fab_command_not_found` &ndash; fix misspelled fabric commands;
@@ -197,6 +204,7 @@ following rules are enabled by default:
* `git_branch_list` &ndash; catches `git branch list` in place of `git branch` and removes created branch; * `git_branch_list` &ndash; catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` &ndash; fixes branch name or creates new branch; * `git_checkout` &ndash; fixes branch name or creates new branch;
* `git_commit_amend` &ndash; offers `git commit --amend` after previous commit; * `git_commit_amend` &ndash; offers `git commit --amend` after previous commit;
* `git_commit_reset` &ndash; offers `git reset HEAD~` after previous commit;
* `git_diff_no_index` &ndash; adds `--no-index` to previous `git diff` on untracked files; * `git_diff_no_index` &ndash; adds `--no-index` to previous `git diff` on untracked files;
* `git_diff_staged` &ndash; adds `--staged` to previous `git diff` with unexpected output; * `git_diff_staged` &ndash; adds `--staged` to previous `git diff` with unexpected output;
* `git_fix_stash` &ndash; fixes `git stash` commands (misspelled subcommand and missing `save`); * `git_fix_stash` &ndash; fixes `git stash` commands (misspelled subcommand and missing `save`);
@@ -226,8 +234,8 @@ following rules are enabled by default:
* `go_run` &ndash; appends `.go` extension when compiling/running Go programs; * `go_run` &ndash; appends `.go` extension when compiling/running Go programs;
* `gradle_no_task` &ndash; fixes not found or ambiguous `gradle` task; * `gradle_no_task` &ndash; fixes not found or ambiguous `gradle` task;
* `gradle_wrapper` &ndash; replaces `gradle` with `./gradlew`; * `gradle_wrapper` &ndash; replaces `gradle` with `./gradlew`;
* `grep_arguments_order` &ndash; fixes grep arguments order for situations like `grep -lir . test`; * `grep_arguments_order` &ndash; fixes `grep` arguments order for situations like `grep -lir . test`;
* `grep_recursive` &ndash; adds `-r` when you trying to `grep` directory; * `grep_recursive` &ndash; adds `-r` when you try to `grep` directory;
* `grunt_task_not_found` &ndash; fixes misspelled `grunt` commands; * `grunt_task_not_found` &ndash; fixes misspelled `grunt` commands;
* `gulp_not_task` &ndash; fixes misspelled `gulp` tasks; * `gulp_not_task` &ndash; fixes misspelled `gulp` tasks;
* `has_exists_script` &ndash; prepends `./` when script/binary exists; * `has_exists_script` &ndash; prepends `./` when script/binary exists;
@@ -239,6 +247,7 @@ following rules are enabled by default:
* `java` &ndash; removes `.java` extension when running Java programs; * `java` &ndash; removes `.java` extension when running Java programs;
* `javac` &ndash; appends missing `.java` when compiling Java files; * `javac` &ndash; appends missing `.java` when compiling Java files;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `long_form_help` &ndash; changes `-h` to `--help` when the short form version is not supported
* `ln_no_hard_link` &ndash; catches hard link creation on directories, suggest symbolic link; * `ln_no_hard_link` &ndash; catches hard link creation on directories, suggest symbolic link;
* `ln_s_order` &ndash; fixes `ln -s` arguments order; * `ln_s_order` &ndash; fixes `ln -s` arguments order;
* `ls_all` &ndash; adds `-A` to `ls` when output is empty; * `ls_all` &ndash; adds `-A` to `ls` when output is empty;
@@ -247,7 +256,7 @@ following rules are enabled by default:
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`; * `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
* `mercurial` &ndash; fixes wrong `hg` commands; * `mercurial` &ndash; fixes wrong `hg` commands;
* `missing_space_before_subcommand` &ndash; fixes command with missing space like `npminstall`; * `missing_space_before_subcommand` &ndash; fixes command with missing space like `npminstall`;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent; * `mkdir_p` &ndash; adds `-p` when you try to create a directory without parent;
* `mvn_no_command` &ndash; adds `clean package` to `mvn`; * `mvn_no_command` &ndash; adds `clean package` to `mvn`;
* `mvn_unknown_lifecycle_phase` &ndash; fixes misspelled lifecycle phases with `mvn`; * `mvn_unknown_lifecycle_phase` &ndash; fixes misspelled lifecycle phases with `mvn`;
* `npm_missing_script` &ndash; fixes `npm` custom script name in `npm run-script <script>`; * `npm_missing_script` &ndash; fixes `npm` custom script name in `npm run-script <script>`;
@@ -256,17 +265,19 @@ following rules are enabled by default:
* `no_command` &ndash; fixes wrong console commands, for example `vom/vim`; * `no_command` &ndash; fixes wrong console commands, for example `vom/vim`;
* `no_such_file` &ndash; creates missing directories with `mv` and `cp` commands; * `no_such_file` &ndash; creates missing directories with `mv` and `cp` commands;
* `open` &ndash; either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`; * `open` &ndash; either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`;
* `pip_install` &ndash; fixes permission issues with `pip install` commands by adding `--user` or prepending `sudo` if necessary;
* `pip_unknown_command` &ndash; fixes wrong `pip` commands, for example `pip instatl/pip install`; * `pip_unknown_command` &ndash; fixes wrong `pip` commands, for example `pip instatl/pip install`;
* `php_s` &ndash; replaces `-s` by `-S` when trying to run a local php server; * `php_s` &ndash; replaces `-s` by `-S` when trying to run a local php server;
* `port_already_in_use` &ndash; kills process that bound port; * `port_already_in_use` &ndash; kills process that bound port;
* `prove_recursively` &ndash; adds `-r` when called with directory; * `prove_recursively` &ndash; adds `-r` when called with directory;
* `python_command` &ndash; prepends `python` when you trying to run not executable/without `./` python script; * `pyenv_no_such_command` &ndash; fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`;
* `python_command` &ndash; prepends `python` when you try to run non-executable/without `./` python script;
* `python_execute` &ndash; appends missing `.py` when executing Python files; * `python_execute` &ndash; appends missing `.py` when executing Python files;
* `quotation_marks` &ndash; fixes uneven usage of `'` and `"` when containing args'; * `quotation_marks` &ndash; fixes uneven usage of `'` and `"` when containing args';
* `path_from_history` &ndash; replaces not found path with similar absolute path from history; * `path_from_history` &ndash; replaces not found path with similar absolute path from history;
* `react_native_command_unrecognized` &ndash; fixes unrecognized `react-native` commands; * `react_native_command_unrecognized` &ndash; fixes unrecognized `react-native` commands;
* `remove_trailing_cedilla` &ndash; remove trailling cedillas `ç`, a common typo for european keyboard layouts; * `remove_trailing_cedilla` &ndash; remove trailling cedillas `ç`, a common typo for european keyboard layouts;
* `rm_dir` &ndash; adds `-rf` when you trying to remove directory; * `rm_dir` &ndash; adds `-rf` when you try to remove a directory;
* `scm_correction` &ndash; corrects wrong scm like `hg log` to `git log`; * `scm_correction` &ndash; corrects wrong scm like `hg log` to `git log`;
* `sed_unterminated_s` &ndash; adds missing '/' to `sed`'s `s` commands; * `sed_unterminated_s` &ndash; adds missing '/' to `sed`'s `s` commands;
* `sl_ls` &ndash; changes `sl` to `ls`; * `sl_ls` &ndash; changes `sl` to `ls`;
@@ -299,13 +310,14 @@ The following rules are enabled by default on specific platforms only:
* `apt_upgrade` &ndash; helps you run `apt upgrade` after `apt list --upgradable`; * `apt_upgrade` &ndash; helps you run `apt upgrade` after `apt list --upgradable`;
* `brew_cask_dependency` &ndash; installs cask dependencies; * `brew_cask_dependency` &ndash; installs cask dependencies;
* `brew_install` &ndash; fixes formula name for `brew install`; * `brew_install` &ndash; fixes formula name for `brew install`;
* `brew_reinstall` &ndash; turns `brew install <formula>` into `brew reinstall <formula>`;
* `brew_link` &ndash; adds `--overwrite --dry-run` if linking fails; * `brew_link` &ndash; adds `--overwrite --dry-run` if linking fails;
* `brew_uninstall` &ndash; adds `--force` to `brew uninstall` if multiple versions were installed; * `brew_uninstall` &ndash; adds `--force` to `brew uninstall` if multiple versions were installed;
* `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`; * `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_update_formula` &ndash; turns `brew update <formula>` into `brew upgrade <formula>`; * `brew_update_formula` &ndash; turns `brew update <formula>` into `brew upgrade <formula>`;
* `dnf_no_such_command` &ndash; fixes mistyped DNF commands; * `dnf_no_such_command` &ndash; fixes mistyped DNF commands;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yaourt` if available); * `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yay` or `yaourt` if available);
* `pacman_not_found` &ndash; fixes package name with `pacman` or `yaourt`. * `pacman_not_found` &ndash; fixes package name with `pacman`, `yay` or `yaourt`.
The following commands are bundled with *The Fuck*, but are not enabled by The following commands are bundled with *The Fuck*, but are not enabled by
default: default:
@@ -381,7 +393,8 @@ Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefu
* `history_limit` &ndash; numeric value of how many history commands will be scanned, like `2000`; * `history_limit` &ndash; numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` &ndash; push fixed command to history, by default `True`; * `alter_history` &ndash; push fixed command to history, by default `True`;
* `wait_slow_command` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list; * `wait_slow_command` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `slow_commands` &ndash; list of slow commands. * `slow_commands` &ndash; list of slow commands;
* `num_close_matches` &ndash; maximum number of close matches to suggest, by default `3`.
An example of `settings.py`: An example of `settings.py`:
@@ -396,6 +409,7 @@ debug = False
history_limit = 9999 history_limit = 9999
wait_slow_command = 20 wait_slow_command = 20
slow_commands = ['react-native', 'gradle'] slow_commands = ['react-native', 'gradle']
num_close_matches = 5
``` ```
Or via environment variables: Or via environment variables:
@@ -411,7 +425,8 @@ rule with lower `priority` will be matched first;
* `THEFUCK_HISTORY_LIMIT` &ndash; how many history commands will be scanned, like `2000`; * `THEFUCK_HISTORY_LIMIT` &ndash; how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` &ndash; push fixed command to history `true/false`; * `THEFUCK_ALTER_HISTORY` &ndash; push fixed command to history `true/false`;
* `THEFUCK_WAIT_SLOW_COMMAND` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list; * `THEFUCK_WAIT_SLOW_COMMAND` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `THEFUCK_SLOW_COMMANDS` &ndash; list of slow commands, like `lein:gradle`. * `THEFUCK_SLOW_COMMANDS` &ndash; list of slow commands, like `lein:gradle`;
* `THEFUCK_NUM_CLOSE_MATCHES` &ndash; maximum number of close matches to suggest, like `5`.
For example: For example:
@@ -423,6 +438,7 @@ export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false' export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100' export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000' export THEFUCK_HISTORY_LIMIT='2000'
export THEFUCK_NUM_CLOSE_MATCHES='5'
``` ```
## Third-party packages with rules ## Third-party packages with rules
@@ -452,7 +468,7 @@ then reading the log.
[![gif with instant mode][instant-mode-gif-link]][instant-mode-gif-link] [![gif with instant mode][instant-mode-gif-link]][instant-mode-gif-link]
Currently, instant mode only supports Python 3 with bash or zsh. Currently, instant mode only supports Python 3 with bash or zsh. zsh's autocorrect function also needs to be disabled in order for thefuck to work properly.
To enable instant mode, add `--enable-experimental-instant-mode` To enable instant mode, add `--enable-experimental-instant-mode`
to the alias initialization in `.bashrc`, `.bash_profile` or `.zshrc`. to the alias initialization in `.bashrc`, `.bash_profile` or `.zshrc`.
@@ -482,4 +498,5 @@ Project License can be found [here](LICENSE.md).
[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 [instant-mode-gif-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example_instant_mode.gif
[homebrew]: http://brew.sh/ [homebrew]: https://brew.sh/
[linuxbrew]: https://linuxbrew.sh/

View File

@@ -6,6 +6,7 @@ environment:
- PYTHON: "C:/Python34" - PYTHON: "C:/Python34"
- PYTHON: "C:/Python35" - PYTHON: "C:/Python35"
- PYTHON: "C:/Python36" - PYTHON: "C:/Python36"
- PYTHON: "C:/Python37"
init: init:
- "ECHO %PYTHON%" - "ECHO %PYTHON%"

View File

@@ -37,7 +37,7 @@ http://github.com/ninjaaron/fast-entry_points
''' '''
from setuptools.command import easy_install from setuptools.command import easy_install
import re import re
TEMPLATE = '''\ TEMPLATE = r'''\
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}' # EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}'
__requires__ = '{3}' __requires__ = '{3}'
@@ -83,7 +83,7 @@ def main():
import shutil import shutil
import sys import sys
dests = sys.argv[1:] or ['.'] dests = sys.argv[1:] or ['.']
filename = re.sub('\.pyc$', '.py', __file__) filename = re.sub(r'\.pyc$', '.py', __file__)
for dst in dests: for dst in dests:
shutil.copy(filename, dst) shutil.copy(filename, dst)

View File

@@ -32,4 +32,6 @@ call('git push --tags', shell=True)
env = os.environ env = os.environ
env['CONVERT_README'] = 'true' env['CONVERT_README'] = 'true'
call('python setup.py sdist bdist_wheel upload', shell=True, env=env) call('rm -rf dist/*')
call('python setup.py sdist bdist_wheel', shell=True, env=env)
call('twine upload dist/*', shell=True, env=env)

View File

@@ -1,4 +1,3 @@
pip
flake8 flake8
pytest pytest
mock mock
@@ -9,3 +8,4 @@ pexpect
pypandoc pypandoc
pytest-benchmark pytest-benchmark
pytest-docker-pexpect pytest-docker-pexpect
twine

View File

@@ -31,7 +31,7 @@ elif (3, 0) < version < (3, 4):
' ({}.{} detected).'.format(*version)) ' ({}.{} detected).'.format(*version))
sys.exit(-1) sys.exit(-1)
VERSION = '3.26' VERSION = '3.29'
install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte'] install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte']
extras_require = {':python_version<"3.4"': ['pathlib2'], extras_require = {':python_version<"3.4"': ['pathlib2'],

25
snapcraft.yaml Normal file
View File

@@ -0,0 +1,25 @@
name: thefuck
version: stable
version-script: git -C parts/thefuck/build describe --abbrev=0 --tags
summary: Magnificent app which corrects your previous console command.
description: |
The Fuck tries to match a rule for the previous command,
creates a new command using the matched rule and runs it.
grade: stable
confinement: classic
apps:
thefuck:
command: bin/thefuck
environment:
PYTHONIOENCODING: utf-8
fuck:
command: bin/fuck
environment:
PYTHONIOENCODING: utf-8
parts:
thefuck:
source: https://github.com/nvbn/thefuck.git
plugin: python

View File

@@ -42,7 +42,7 @@ def no_cache(monkeypatch):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def functional(request): def functional(request):
if request.node.get_marker('functional') \ if request.node.get_closest_marker('functional') \
and not request.config.getoption('enable_functional'): and not request.config.getoption('enable_functional'):
pytest.skip('functional tests are disabled') pytest.skip('functional tests are disabled')

View File

@@ -1,45 +0,0 @@
import pytest
import time
dockerfile = u'''
FROM python:3
RUN adduser --disabled-password --gecos '' test
ENV SEED "{seed}"
WORKDIR /src
USER test
RUN echo 'eval $(thefuck --alias)' > /home/test/.bashrc
RUN echo > /home/test/.bash_history
RUN git config --global user.email "you@example.com"
RUN git config --global user.name "Your Name"
USER root
'''.format(seed=time.time())
def plot(proc, TIMEOUT):
proc.sendline(u'cd /home/test/')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'No fucks given'])
proc.sendline(u'git init')
proc.sendline(u'git add .')
proc.sendline(u'git commit -a -m init')
proc.sendline(u'git brnch')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'git branch'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'master'])
proc.sendline(u'echo test')
proc.sendline(u'echo tst')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'echo test'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'test'])
@pytest.mark.functional
@pytest.mark.benchmark(min_rounds=10)
def test_performance(spawnu, TIMEOUT, benchmark):
proc = spawnu(u'thefuck/python3-bash-performance',
dockerfile, u'bash')
proc.sendline(u'pip install /src')
proc.sendline(u'su test')
assert benchmark(plot, proc, TIMEOUT) is None

View File

@@ -0,0 +1,58 @@
# -*- encoding: utf-8 -*-
from mock import Mock, patch
from psutil import AccessDenied, TimeoutExpired
from thefuck.output_readers import rerun
class TestRerun(object):
def setup_method(self, test_method):
self.patcher = patch('thefuck.output_readers.rerun.Process')
process_mock = self.patcher.start()
self.proc_mock = process_mock.return_value = Mock()
def teardown_method(self, test_method):
self.patcher.stop()
@patch('thefuck.output_readers.rerun._wait_output', return_value=False)
@patch('thefuck.output_readers.rerun.Popen')
def test_get_output(self, popen_mock, wait_output_mock):
popen_mock.return_value.stdout.read.return_value = b'output'
assert rerun.get_output('', '') is None
wait_output_mock.assert_called_once()
def test_wait_output_is_slow(self, settings):
assert rerun._wait_output(Mock(), True)
self.proc_mock.wait.assert_called_once_with(settings.wait_slow_command)
def test_wait_output_is_not_slow(self, settings):
assert rerun._wait_output(Mock(), False)
self.proc_mock.wait.assert_called_once_with(settings.wait_command)
@patch('thefuck.output_readers.rerun._kill_process')
def test_wait_output_timeout(self, kill_process_mock):
self.proc_mock.wait.side_effect = TimeoutExpired(3)
self.proc_mock.children.return_value = []
assert not rerun._wait_output(Mock(), False)
kill_process_mock.assert_called_once_with(self.proc_mock)
@patch('thefuck.output_readers.rerun._kill_process')
def test_wait_output_timeout_children(self, kill_process_mock):
self.proc_mock.wait.side_effect = TimeoutExpired(3)
self.proc_mock.children.return_value = [Mock()] * 2
assert not rerun._wait_output(Mock(), False)
assert kill_process_mock.call_count == 3
def test_kill_process(self):
proc = Mock()
rerun._kill_process(proc)
proc.kill.assert_called_once_with()
@patch('thefuck.output_readers.rerun.logs')
def test_kill_process_access_denied(self, logs_mock):
proc = Mock()
proc.kill.side_effect = AccessDenied()
rerun._kill_process(proc)
proc.kill.assert_called_once_with()
logs_mock.debug.assert_called_once()

View File

@@ -9,7 +9,7 @@ def output():
'If you meant to search for a literal string, run ag with -Q\n') 'If you meant to search for a literal string, run ag with -Q\n')
@pytest.mark.parametrize('script', ['ag \(']) @pytest.mark.parametrize('script', ['ag \\('])
def test_match(script, output): def test_match(script, output):
assert match(Command(script, output)) assert match(Command(script, output))
@@ -20,6 +20,6 @@ def test_not_match(script):
@pytest.mark.parametrize('script, new_cmd', [ @pytest.mark.parametrize('script, new_cmd', [
('ag \(', 'ag -Q \(')]) ('ag \\(', 'ag -Q \\(')])
def test_get_new_command(script, new_cmd, output): def test_get_new_command(script, new_cmd, output):
assert get_new_command((Command(script, output))) == new_cmd assert get_new_command((Command(script, output))) == new_cmd

View File

@@ -0,0 +1,44 @@
import pytest
from thefuck.rules.az_cli import match, get_new_command
from thefuck.types import Command
no_suggestions = '''\
az provider: error: the following arguments are required: _subcommand
usage: az provider [-h] {list,show,register,unregister,operation} ...
'''
misspelled_command = '''\
az: 'providers' is not in the 'az' command group. See 'az --help'.
The most similar choice to 'providers' is:
provider
'''
misspelled_subcommand = '''\
az provider: 'lis' is not in the 'az provider' command group. See 'az provider --help'.
The most similar choice to 'lis' is:
list
'''
@pytest.mark.parametrize('command', [
Command('az providers', misspelled_command),
Command('az provider lis', misspelled_subcommand)])
def test_match(command):
assert match(command)
def test_not_match():
assert not match(Command('az provider', no_suggestions))
@pytest.mark.parametrize('command, result', [
(Command('az providers list', misspelled_command), ['az provider list']),
(Command('az provider lis', misspelled_subcommand), ['az provider list'])
])
def test_get_new_command(command, result):
assert get_new_command(command) == result

View File

@@ -0,0 +1,28 @@
import pytest
from thefuck.types import Command
from thefuck.rules.brew_reinstall import get_new_command, match
output = ("Warning: thefuck 9.9 is already installed and up-to-date\nTo "
"reinstall 9.9, run `brew reinstall thefuck`")
def test_match():
command = Command('brew install thefuck', output)
assert match(command)
@pytest.mark.parametrize('script', [
'brew reinstall thefuck',
'brew install foo'])
def test_not_match(script):
assert not match(Command(script, ''))
@pytest.mark.parametrize('script, formula, ', [
('brew install foo', 'foo'),
('brew install bar zap', 'bar zap')])
def test_get_new_command(script, formula):
command = Command(script, output)
new_command = 'brew reinstall {}'.format(formula)
assert get_new_command(command) == new_command

View File

@@ -0,0 +1,39 @@
import pytest
from thefuck.rules.cat_dir import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def isdir(mocker):
return mocker.patch('thefuck.rules.cat_dir'
'.os.path.isdir')
@pytest.mark.parametrize('command', [
Command('cat foo', 'cat: foo: Is a directory\n'),
Command('cat /foo/bar/', 'cat: /foo/bar/: Is a directory\n'),
Command('cat cat/', 'cat: cat/: Is a directory\n'),
])
def test_match(command, isdir):
isdir.return_value = True
assert match(command)
@pytest.mark.parametrize('command', [
Command('cat foo', 'foo bar baz'),
Command('cat foo bar', 'foo bar baz'),
Command('notcat foo bar', 'some output'),
])
def test_not_match(command, isdir):
isdir.return_value = False
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('cat foo', 'cat: foo: Is a directory\n'), 'ls foo'),
(Command('cat /foo/bar/', 'cat: /foo/bar/: Is a directory\n'), 'ls /foo/bar/'),
(Command('cat cat', 'cat: cat: Is a directory\n'), 'ls cat'),
])
def test_get_new_command(command, new_command):
isdir.return_value = True
assert get_new_command(command) == new_command

View File

@@ -0,0 +1,37 @@
from thefuck.rules.docker_login import match, get_new_command
from thefuck.types import Command
def test_match():
err_response1 = """
Sending build context to Docker daemon 118.8kB
Step 1/6 : FROM foo/bar:fdb7c6d
pull access denied for foo/bar, repository does not exist or may require 'docker login'
"""
assert match(Command('docker build -t artifactory:9090/foo/bar:fdb7c6d .', err_response1))
err_response2 = """
The push refers to repository [artifactory:9090/foo/bar]
push access denied for foo/bar, repository does not exist or may require 'docker login'
"""
assert match(Command('docker push artifactory:9090/foo/bar:fdb7c6d', err_response2))
err_response3 = """
docker push artifactory:9090/foo/bar:fdb7c6d
The push refers to repository [artifactory:9090/foo/bar]
9c29c7ad209d: Preparing
71f3ad53dfe0: Preparing
f58ee068224c: Preparing
aeddc924d0f7: Preparing
c2040e5d6363: Preparing
4d42df4f350f: Preparing
35723dab26f9: Preparing
71f3ad53dfe0: Pushed
cb95fa0faeb1: Layer already exists
"""
assert not match(Command('docker push artifactory:9090/foo/bar:fdb7c6d', err_response3))
def test_get_new_command():
assert get_new_command(Command('docker build -t artifactory:9090/foo/bar:fdb7c6d .', '')) == 'docker login && docker build -t artifactory:9090/foo/bar:fdb7c6d .'
assert get_new_command(Command('docker push artifactory:9090/foo/bar:fdb7c6d', '')) == 'docker login && docker push artifactory:9090/foo/bar:fdb7c6d'

View File

@@ -4,7 +4,6 @@ from thefuck.rules.git_checkout import match, get_branches, get_new_command
from thefuck.types import Command from thefuck.types import Command
@pytest.fixture
def did_not_match(target, did_you_forget=False): def did_not_match(target, did_you_forget=False):
error = ("error: pathspec '{}' did not match any " error = ("error: pathspec '{}' did not match any "
"file(s) known to git.".format(target)) "file(s) known to git.".format(target))

View File

@@ -0,0 +1,25 @@
import pytest
from thefuck.rules.git_commit_reset import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize('script, output', [
('git commit -m "test"', 'test output'),
('git commit', '')])
def test_match(output, script):
assert match(Command(script, output))
@pytest.mark.parametrize('script', [
'git branch foo',
'git checkout feature/test_commit',
'git push'])
def test_not_match(script):
assert not match(Command(script, ''))
@pytest.mark.parametrize('script', [
('git commit -m "test commit"'),
('git commit')])
def test_get_new_command(script):
assert get_new_command(Command(script, '')) == 'git reset HEAD~'

View File

@@ -3,24 +3,22 @@ from thefuck.rules.git_merge import match, get_new_command
from thefuck.types import Command from thefuck.types import Command
@pytest.fixture output = 'merge: local - not something we can merge\n\n' \
def output():
return 'merge: local - not something we can merge\n\n' \
'Did you mean this?\n\tremote/local' 'Did you mean this?\n\tremote/local'
def test_match(output): def test_match():
assert match(Command('git merge test', output)) assert match(Command('git merge test', output))
assert not match(Command('git merge master', '')) assert not match(Command('git merge master', ''))
assert not match(Command('ls', output)) assert not match(Command('ls', output))
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, new_command', [
(Command('git merge local', output()), (Command('git merge local', output),
'git merge remote/local'), 'git merge remote/local'),
(Command('git merge -m "test" local', output()), (Command('git merge -m "test" local', output),
'git merge -m "test" remote/local'), 'git merge -m "test" remote/local'),
(Command('git merge -m "test local" local', output()), (Command('git merge -m "test local" local', output),
'git merge -m "test local" remote/local')]) 'git merge -m "test local" remote/local')])
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

@@ -3,23 +3,21 @@ from thefuck.rules.git_merge_unrelated import match, get_new_command
from thefuck.types import Command from thefuck.types import Command
@pytest.fixture output = 'fatal: refusing to merge unrelated histories'
def output():
return 'fatal: refusing to merge unrelated histories'
def test_match(output): def test_match():
assert match(Command('git merge test', output)) assert match(Command('git merge test', output))
assert not match(Command('git merge master', '')) assert not match(Command('git merge master', ''))
assert not match(Command('ls', output)) assert not match(Command('ls', output))
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, new_command', [
(Command('git merge local', output()), (Command('git merge local', output),
'git merge local --allow-unrelated-histories'), 'git merge local --allow-unrelated-histories'),
(Command('git merge -m "test" local', output()), (Command('git merge -m "test" local', output),
'git merge -m "test" local --allow-unrelated-histories'), 'git merge -m "test" local --allow-unrelated-histories'),
(Command('git merge -m "test local" local', output()), (Command('git merge -m "test local" local', output),
'git merge -m "test local" local --allow-unrelated-histories')]) 'git merge -m "test local" local --allow-unrelated-histories')])
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

@@ -66,6 +66,10 @@ def test_not_match(output, script, branch_name):
('git -c test=test push --quiet origin', 'master', ('git -c test=test push --quiet origin', 'master',
'git -c test=test push --set-upstream origin master --quiet'), 'git -c test=test push --set-upstream origin master --quiet'),
('git push', "test's", ('git push', "test's",
"git push --set-upstream origin test\\'s")]) "git push --set-upstream origin test\\'s"),
('git push --force', 'master',
'git push --set-upstream origin master --force'),
('git push --force-with-lease', 'master',
'git push --set-upstream origin master --force-with-lease')])
def test_get_new_command(output, script, branch_name, new_command): def test_get_new_command(output, script, branch_name, new_command):
assert get_new_command(Command(script, output)) == new_command assert get_new_command(Command(script, output)) == new_command

View File

@@ -0,0 +1,22 @@
import pytest
from thefuck.rules.long_form_help import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize('output', [
'Try \'grep --help\' for more information.'])
def test_match(output):
assert match(Command('grep -h', output))
def test_not_match():
assert not match(Command('', ''))
@pytest.mark.parametrize('before, after', [
('grep -h', 'grep --help'),
('tar -h', 'tar --help'),
('docker run -h', 'docker run --help'),
('cut -h', 'cut --help')])
def test_get_new_command(before, after):
assert get_new_command(Command(before, '')) == after

View File

@@ -6,7 +6,7 @@ from thefuck.types import Command
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def get_all_executables(mocker): def get_all_executables(mocker):
mocker.patch('thefuck.rules.no_command.get_all_executables', mocker.patch('thefuck.rules.no_command.get_all_executables',
return_value=['vim', 'fsck', 'git', 'go']) return_value=['vim', 'fsck', 'git', 'go', 'python'])
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -20,6 +20,7 @@ def history_without_current(mocker):
@pytest.mark.parametrize('script, output', [ @pytest.mark.parametrize('script, output', [
('vom file.py', 'vom: not found'), ('vom file.py', 'vom: not found'),
('fucck', 'fucck: not found'), ('fucck', 'fucck: not found'),
('puthon', "'puthon' is not recognized as an internal or external command"),
('got commit', 'got: command not found')]) ('got commit', 'got: command not found')])
def test_match(mocker, script, output): def test_match(mocker, script, output):
mocker.patch('thefuck.rules.no_command.which', return_value=None) mocker.patch('thefuck.rules.no_command.which', return_value=None)

View File

@@ -11,6 +11,7 @@ extra/llvm35 3.5.2-13/usr/bin/llc'''
@pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True), @pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True),
reason='Skip if pacman is not available') reason='Skip if pacman is not available')
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command('yay -S llc', 'error: target not found: llc'),
Command('yaourt -S llc', 'error: target not found: llc'), Command('yaourt -S llc', 'error: target not found: llc'),
Command('pacman llc', 'error: target not found: llc'), Command('pacman llc', 'error: target not found: llc'),
Command('sudo pacman llc', 'error: target not found: llc')]) Command('sudo pacman llc', 'error: target not found: llc')])
@@ -19,6 +20,7 @@ def test_match(command):
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command('yay -S llc', 'error: target not found: llc'),
Command('yaourt -S llc', 'error: target not found: llc'), Command('yaourt -S llc', 'error: target not found: llc'),
Command('pacman llc', 'error: target not found: llc'), Command('pacman llc', 'error: target not found: llc'),
Command('sudo pacman llc', 'error: target not found: llc')]) Command('sudo pacman llc', 'error: target not found: llc')])
@@ -31,6 +33,7 @@ def test_match_mocked(subp_mock, command):
@pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True), @pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True),
reason='Skip if pacman is not available') reason='Skip if pacman is not available')
@pytest.mark.parametrize('command, fixed', [ @pytest.mark.parametrize('command, fixed', [
(Command('yay -S llc', 'error: target not found: llc'), ['yay -S extra/llvm', 'yay -S extra/llvm35']),
(Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']), (Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']),
(Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']), (Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']),
(Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])]) (Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])])
@@ -39,6 +42,7 @@ def test_get_new_command(command, fixed):
@pytest.mark.parametrize('command, fixed', [ @pytest.mark.parametrize('command, fixed', [
(Command('yay -S llc', 'error: target not found: llc'), ['yay -S extra/llvm', 'yay -S extra/llvm35']),
(Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']), (Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']),
(Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']), (Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']),
(Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])]) (Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])])

View File

@@ -0,0 +1,27 @@
# -*- coding: UTF-8 -*-
from thefuck.rules.pip_install import match, get_new_command
from thefuck.types import Command
def test_match():
response1 = """
Could not install packages due to an EnvironmentError: [Errno 13] Permission denied: '/Library/Python/2.7/site-packages/entrypoints.pyc'
Consider using the `--user` option or check the permissions.
"""
assert match(Command('pip install -r requirements.txt', response1))
response2 = """
Collecting bacon
Downloading https://files.pythonhosted.org/packages/b2/81/19fb79139ee71c8bc4e5a444546f318e2b87253b8939ec8a7e10d63b7341/bacon-0.3.1.zip (11.0MB)
100% |████████████████████████████████| 11.0MB 3.0MB/s
Installing collected packages: bacon
Running setup.py install for bacon ... done
Successfully installed bacon-0.3.1
"""
assert not match(Command('pip install bacon', response2))
def test_get_new_command():
assert get_new_command(Command('pip install -r requirements.txt', '')) == 'pip install --user -r requirements.txt'
assert get_new_command(Command('pip install bacon', '')) == 'pip install --user bacon'
assert get_new_command(Command('pip install --user -r requirements.txt', '')) == 'sudo pip install -r requirements.txt'

View File

@@ -0,0 +1,52 @@
import pytest
from thefuck.rules.pyenv_no_such_command import get_new_command, match
from thefuck.types import Command
@pytest.fixture
def output(pyenv_cmd):
return "pyenv: no such command `{}'".format(pyenv_cmd)
@pytest.fixture(autouse=True)
def Popen(mocker):
mock = mocker.patch('thefuck.rules.pyenv_no_such_command.Popen')
mock.return_value.stdout.readlines.return_value = (
b'--version\nactivate\ncommands\ncompletions\ndeactivate\nexec_\n'
b'global\nhelp\nhooks\ninit\ninstall\nlocal\nprefix_\n'
b'realpath.dylib\nrehash\nroot\nshell\nshims\nuninstall\nversion_\n'
b'version-file\nversion-file-read\nversion-file-write\nversion-name_\n'
b'version-origin\nversions\nvirtualenv\nvirtualenv-delete_\n'
b'virtualenv-init\nvirtualenv-prefix\nvirtualenvs_\n'
b'virtualenvwrapper\nvirtualenvwrapper_lazy\nwhence\nwhich_\n'
).split()
return mock
@pytest.mark.parametrize('script, pyenv_cmd', [
('pyenv globe', 'globe'),
('pyenv intall 3.8.0', 'intall'),
('pyenv list', 'list'),
])
def test_match(script, pyenv_cmd, output):
assert match(Command(script, output=output))
@pytest.mark.parametrize('script, output', [
('pyenv global', 'system'),
('pyenv versions', ' 3.7.0\n 3.7.1\n* 3.7.2\n'),
('pyenv install --list', ' 3.7.0\n 3.7.1\n 3.7.2\n'),
])
def test_not_match(script, output):
assert not match(Command(script, output=output))
@pytest.mark.parametrize('script, pyenv_cmd, result', [
('pyenv globe', 'globe', 'pyenv global'),
('pyenv intall 3.8.0', 'intall', 'pyenv install 3.8.0'),
('pyenv list', 'list', 'pyenv install --list'),
('pyenv remove 3.8.0', 'remove', 'pyenv uninstall 3.8.0'),
])
def test_get_new_command(script, pyenv_cmd, output, result):
assert result in get_new_command(Command(script, output))

View File

@@ -3,25 +3,31 @@ from thefuck.rules.touch import match, get_new_command
from thefuck.types import Command from thefuck.types import Command
@pytest.fixture def output(is_bsd):
def output(): if is_bsd:
return "touch: cannot touch '/a/b/c':" \ return "touch: /a/b/c: No such file or directory"
" No such file or directory" return "touch: cannot touch '/a/b/c': No such file or directory"
def test_match(output): @pytest.mark.parametrize('script, is_bsd', [
command = Command('touch /a/b/c', output) ('touch /a/b/c', False),
('touch /a/b/c', True)])
def test_match(script, is_bsd):
command = Command(script, output(is_bsd))
assert match(command) assert match(command)
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command('touch /a/b/c', ''), Command('touch /a/b/c', ''),
Command('ls /a/b/c', output())]) Command('ls /a/b/c', output(False))])
def test_not_match(command): def test_not_match(command):
assert not match(command) assert not match(command)
def test_get_new_command(output): @pytest.mark.parametrize('script, is_bsd', [
command = Command('touch /a/b/c', output) ('touch /a/b/c', False),
('touch /a/b/c', True)])
def test_get_new_command(script, is_bsd):
command = Command(script, output(is_bsd))
fixed_command = get_new_command(command) fixed_command = get_new_command(command)
assert fixed_command == 'mkdir -p /a/b && touch /a/b/c' assert fixed_command == 'mkdir -p /a/b && touch /a/b/c'

View File

@@ -11,6 +11,11 @@ class TestBash(object):
def shell(self): def shell(self):
return Bash() return Bash()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.bash.Popen')
return mock
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def shell_aliases(self): def shell_aliases(self):
os.environ['TF_SHELL_ALIASES'] = ( os.environ['TF_SHELL_ALIASES'] = (
@@ -73,3 +78,13 @@ class TestBash(object):
config_exists): config_exists):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically assert not shell.how_to_configure().can_configure_automatically
def test_info(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = [b'3.5.9']
assert shell.info() == 'Bash 3.5.9'
def test_get_version_error(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = OSError
with pytest.raises(OSError):
shell._get_version()
assert Popen.call_args[0][0] == ['bash', '-c', 'echo $BASH_VERSION']

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest import pytest
from thefuck.const import ARGUMENT_PLACEHOLDER
from thefuck.shells import Fish from thefuck.shells import Fish
@@ -16,7 +17,8 @@ class TestFish(object):
mock.return_value.stdout.read.side_effect = [( mock.return_value.stdout.read.side_effect = [(
b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n' b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby'), b'man\nmath\npopd\npushd\nruby'),
b'alias fish_key_reader /usr/bin/fish_key_reader\nalias g git'] (b'alias fish_key_reader /usr/bin/fish_key_reader\nalias g git\n'
b'alias alias_with_equal_sign=echo\ninvalid_alias'), b'func1\nfunc2', b'']
return mock return mock
@pytest.mark.parametrize('key, value', [ @pytest.mark.parametrize('key, value', [
@@ -27,7 +29,8 @@ class TestFish(object):
('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, key, value): def test_get_overridden_aliases(self, shell, os_environ, key, value):
os_environ[key] = value os_environ[key] = value
assert shell._get_overridden_aliases() == {'cd', 'cut', 'git', 'grep', overridden = shell._get_overridden_aliases()
assert set(overridden) == {'cd', 'cut', 'git', 'grep',
'ls', 'man', 'open', 'sed'} 'ls', 'man', 'open', 'sed'}
@pytest.mark.parametrize('before, after', [ @pytest.mark.parametrize('before, after', [
@@ -69,7 +72,9 @@ class TestFish(object):
'pushd': 'pushd', 'pushd': 'pushd',
'ruby': 'ruby', 'ruby': 'ruby',
'g': 'git', 'g': 'git',
'fish_key_reader': '/usr/bin/fish_key_reader'} 'fish_key_reader': '/usr/bin/fish_key_reader',
'alias_with_equal_sign': 'echo'}
assert shell.get_aliases() == {'func1': 'func1', 'func2': 'func2'}
def test_app_alias(self, shell): def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias('fuck') assert 'function fuck' in shell.app_alias('fuck')
@@ -78,6 +83,7 @@ class TestFish(object):
assert 'TF_SHELL=fish' in shell.app_alias('fuck') assert 'TF_SHELL=fish' in shell.app_alias('fuck')
assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck')
assert ARGUMENT_PLACEHOLDER in shell.app_alias('fuck')
def test_app_alias_alter_history(self, settings, shell): def test_app_alias_alter_history(self, settings, shell):
settings.alter_history = True settings.alter_history = True
@@ -109,3 +115,18 @@ class TestFish(object):
config_exists): config_exists):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically assert not shell.how_to_configure().can_configure_automatically
def test_get_version(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = [b'fish, version 3.5.9\n']
assert shell._get_version() == '3.5.9'
assert Popen.call_args[0][0] == ['fish', '--version']
@pytest.mark.parametrize('side_effect, exception', [
([b'\n'], IndexError),
(OSError('file not found'), OSError),
])
def test_get_version_error(self, side_effect, exception, shell, Popen):
Popen.return_value.stdout.read.side_effect = side_effect
with pytest.raises(exception):
shell._get_version()
assert Popen.call_args[0][0] == ['fish', '--version']

View File

@@ -43,3 +43,14 @@ class TestGeneric(object):
def test_how_to_configure(self, shell): def test_how_to_configure(self, shell):
assert shell.how_to_configure() is None assert shell.how_to_configure() is None
@pytest.mark.parametrize('side_effect, expected_info, warn', [
([u'3.5.9'], u'Generic Shell 3.5.9', False),
([OSError], u'Generic Shell', True),
])
def test_info(self, side_effect, expected_info, warn, shell, mocker):
warn_mock = mocker.patch('thefuck.shells.generic.warn')
shell._get_version = mocker.Mock(side_effect=side_effect)
assert shell.info() == expected_info
assert warn_mock.called is warn
assert shell._get_version.called

View File

@@ -10,6 +10,11 @@ class TestPowershell(object):
def shell(self): def shell(self):
return Powershell() return Powershell()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.powershell.Popen')
return mock
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('ls', 'cd') == '(ls) -and (cd)' assert shell.and_('ls', 'cd') == '(ls) -and (cd)'
@@ -20,3 +25,20 @@ class TestPowershell(object):
def test_how_to_configure(self, shell): def test_how_to_configure(self, shell):
assert not shell.how_to_configure().can_configure_automatically assert not shell.how_to_configure().can_configure_automatically
@pytest.mark.parametrize('side_effect, expected_version, call_args', [
([b'''Major Minor Build Revision
----- ----- ----- --------
5 1 17763 316 \n'''], 'PowerShell 5.1.17763.316', ['powershell.exe']),
([IOError, b'PowerShell 6.1.2\n'], 'PowerShell 6.1.2', ['powershell.exe', 'pwsh'])])
def test_info(self, side_effect, expected_version, call_args, shell, Popen):
Popen.return_value.stdout.read.side_effect = side_effect
assert shell.info() == expected_version
assert Popen.call_count == len(call_args)
assert all([Popen.call_args_list[i][0][0][0] == call_arg for i, call_arg in enumerate(call_args)])
def test_get_version_error(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = RuntimeError
with pytest.raises(RuntimeError):
shell._get_version()
assert Popen.call_args[0][0] == ['powershell.exe', '$PSVersionTable.PSVersion']

View File

@@ -61,3 +61,17 @@ class TestTcsh(object):
config_exists): config_exists):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically assert not shell.how_to_configure().can_configure_automatically
def test_info(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = [
b'tcsh 6.20.00 (Astron) 2016-11-24 (unknown-unknown-bsd44) \n']
assert shell.info() == 'Tcsh 6.20.00'
assert Popen.call_args[0][0] == ['tcsh', '--version']
@pytest.mark.parametrize('side_effect, exception', [
([b'\n'], IndexError), (OSError, OSError)])
def test_get_version_error(self, side_effect, exception, shell, Popen):
Popen.return_value.stdout.read.side_effect = side_effect
with pytest.raises(exception):
shell._get_version()
assert Popen.call_args[0][0] == ['tcsh', '--version']

View File

@@ -11,6 +11,11 @@ class TestZsh(object):
def shell(self): def shell(self):
return Zsh() return Zsh()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.zsh.Popen')
return mock
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def shell_aliases(self): def shell_aliases(self):
os.environ['TF_SHELL_ALIASES'] = ( os.environ['TF_SHELL_ALIASES'] = (
@@ -68,3 +73,13 @@ class TestZsh(object):
config_exists): config_exists):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically assert not shell.how_to_configure().can_configure_automatically
def test_info(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = [b'3.5.9']
assert shell.info() == 'ZSH 3.5.9'
def test_get_version_error(self, shell, Popen):
Popen.return_value.stdout.read.side_effect = OSError
with pytest.raises(OSError):
shell._get_version()
assert Popen.call_args[0][0] == ['zsh', '-c', 'echo $ZSH_VERSION']

View File

@@ -53,7 +53,8 @@ class TestSettingsFromEnv(object):
'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',
'THEFUCK_NUM_CLOSE_MATCHES': '359'})
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']
@@ -63,6 +64,7 @@ class TestSettingsFromEnv(object):
assert settings.priority == {'bash': 10, 'vim': 15} assert settings.priority == {'bash': 10, 'vim': 15}
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']
assert settings.num_close_matches == 359
def test_from_env_with_DEFAULT(self, os_environ, settings): def test_from_env_with_DEFAULT(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@@ -2,11 +2,11 @@
import pytest import pytest
import warnings import warnings
from mock import Mock from mock import Mock, call, patch
from thefuck.utils import default_settings, \ from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \ memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, \ get_all_matched_commands, is_app, for_app, cache, \
get_valid_history_without_current, _cache get_valid_history_without_current, _cache, get_close_matches
from thefuck.types import Command from thefuck.types import Command
@@ -50,6 +50,18 @@ class TestGetClosest(object):
fallback_to_first=False) is None fallback_to_first=False) is None
class TestGetCloseMatches(object):
@patch('thefuck.utils.difflib_get_close_matches')
def test_call_with_n(self, difflib_mock):
get_close_matches('', [], 1)
assert difflib_mock.call_args[0][2] == 1
@patch('thefuck.utils.difflib_get_close_matches')
def test_call_without_n(self, difflib_mock, settings):
get_close_matches('', [])
assert difflib_mock.call_args[0][2] == settings.get('num_close_matches')
@pytest.fixture @pytest.fixture
def get_aliases(mocker): def get_aliases(mocker):
mocker.patch('thefuck.shells.shell.get_aliases', mocker.patch('thefuck.shells.shell.get_aliases',
@@ -64,6 +76,24 @@ def test_get_all_executables():
assert 'fuck' not in all_callables assert 'fuck' not in all_callables
@pytest.fixture
def os_environ_pathsep(monkeypatch, path, pathsep):
env = {'PATH': path}
monkeypatch.setattr('os.environ', env)
monkeypatch.setattr('os.pathsep', pathsep)
return env
@pytest.mark.usefixtures('no_memoize', 'os_environ_pathsep')
@pytest.mark.parametrize('path, pathsep', [
('/foo:/bar:/baz:/foo/bar', ':'),
(r'C:\\foo;C:\\bar;C:\\baz;C:\\foo\\bar', ';')])
def test_get_all_executables_pathsep(path, pathsep):
with patch('thefuck.utils.Path') as Path_mock:
get_all_executables()
Path_mock.assert_has_calls([call(p) for p in path.split(pathsep)], True)
@pytest.mark.parametrize('args, result', [ @pytest.mark.parametrize('args, result', [
(('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'), (('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'),
(('git brnch', 'brnch', 'branch'), 'git branch')]) (('git brnch', 'brnch', 'branch'), 'git branch')])

View File

@@ -55,7 +55,7 @@ class Parser(object):
"""It's too dangerous to use `-y` and `-r` together.""" """It's too dangerous to use `-y` and `-r` together."""
group = self._parser.add_mutually_exclusive_group() group = self._parser.add_mutually_exclusive_group()
group.add_argument( group.add_argument(
'-y', '--yes', '-y', '--yes', '--yeah',
action='store_true', action='store_true',
help='execute fixed command without confirmation') help='execute fixed command without confirmation')
group.add_argument( group.add_argument(

View File

@@ -95,7 +95,8 @@ class Settings(dict):
return self._rules_from_env(val) return self._rules_from_env(val)
elif attr == 'priority': elif attr == 'priority':
return dict(self._priority_from_env(val)) return dict(self._priority_from_env(val))
elif attr in ('wait_command', 'history_limit', 'wait_slow_command'): elif attr in ('wait_command', 'history_limit', 'wait_slow_command',
'num_close_matches'):
return int(val) return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug', elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history', 'instant_mode'): 'alter_history', 'instant_mode'):

View File

@@ -42,6 +42,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'./gradlew', 'vagrant'], './gradlew', 'vagrant'],
'repeat': False, 'repeat': False,
'instant_mode': False, 'instant_mode': False,
'num_close_matches': 3,
'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',
@@ -56,7 +57,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command', 'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands', 'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat', 'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode'} 'THEFUCK_INSTANT_MODE': 'instant_mode',
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches'}
SETTINGS_HEADER = u"""# The Fuck settings file SETTINGS_HEADER = u"""# The Fuck settings file
# #
@@ -81,3 +83,7 @@ LOG_SIZE_IN_BYTES = 1024 * 1024
LOG_SIZE_TO_CLEAN = 10 * 1024 LOG_SIZE_TO_CLEAN = 10 * 1024
DIFF_WITH_ALIAS = 0.5 DIFF_WITH_ALIAS = 0.5
SHELL_LOGGER_SOCKET_ENV = 'SHELL_LOGGER_SOCKET'
SHELL_LOGGER_LIMIT = 5

View File

@@ -8,6 +8,7 @@ import sys # noqa: E402
from .. import logs # noqa: E402 from .. import logs # noqa: E402
from ..argument_parser import Parser # noqa: E402 from ..argument_parser import Parser # noqa: E402
from ..utils import get_installation_info # noqa: E402 from ..utils import get_installation_info # noqa: E402
from ..shells import shell # noqa: E402
from .alias import print_alias # noqa: E402 from .alias import print_alias # noqa: E402
from .fix_command import fix_command # noqa: E402 from .fix_command import fix_command # noqa: E402
@@ -20,7 +21,7 @@ def main():
parser.print_help() parser.print_help()
elif known_args.version: elif known_args.version:
logs.version(get_installation_info().version, logs.version(get_installation_info().version,
sys.version.split()[0]) sys.version.split()[0], shell.info())
elif known_args.command or 'TF_HISTORY' in os.environ: elif known_args.command or 'TF_HISTORY' in os.environ:
fix_command(known_args) fix_command(known_args)
elif known_args.alias: elif known_args.alias:

View File

@@ -134,7 +134,8 @@ def configured_successfully(configuration_details):
reload=configuration_details.reload)) reload=configuration_details.reload))
def version(thefuck_version, python_version): def version(thefuck_version, python_version, shell_info):
sys.stderr.write( sys.stderr.write(
u'The Fuck {} using Python {}\n'.format(thefuck_version, u'The Fuck {} using Python {} and {}\n'.format(thefuck_version,
python_version)) python_version,
shell_info))

View File

@@ -1,5 +1,5 @@
from ..conf import settings from ..conf import settings
from . import read_log, rerun from . import read_log, rerun, shell_logger
def get_output(script, expanded): def get_output(script, expanded):
@@ -12,6 +12,8 @@ def get_output(script, expanded):
:rtype: str :rtype: str
""" """
if shell_logger.is_available():
return shell_logger.get_output(script)
if settings.instant_mode: if settings.instant_mode:
return read_log.get_output(script) return read_log.get_output(script)
else: else:

View File

@@ -1,11 +1,25 @@
import os import os
import shlex import shlex
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from psutil import Process, TimeoutExpired from psutil import AccessDenied, Process, TimeoutExpired
from .. import logs from .. import logs
from ..conf import settings from ..conf import settings
def _kill_process(proc):
"""Tries to kill the process otherwise just logs a debug message, the
process will be killed when thefuck terminates.
:type proc: Process
"""
try:
proc.kill()
except AccessDenied:
logs.debug(u'Rerun: process PID {} ({}) could not be terminated'.format(
proc.pid, proc.exe()))
def _wait_output(popen, is_slow): def _wait_output(popen, is_slow):
"""Returns `True` if we can get output of the command in the """Returns `True` if we can get output of the command in the
`settings.wait_command` time. `settings.wait_command` time.
@@ -23,8 +37,8 @@ def _wait_output(popen, is_slow):
return True return True
except TimeoutExpired: except TimeoutExpired:
for child in proc.children(recursive=True): for child in proc.children(recursive=True):
child.kill() _kill_process(child)
proc.kill() _kill_process(proc)
return False return False

View File

@@ -0,0 +1,60 @@
import json
import os
import socket
try:
from shutil import get_terminal_size
except ImportError:
from backports.shutil_get_terminal_size import get_terminal_size
import pyte
from .. import const, logs
def _get_socket_path():
return os.environ.get(const.SHELL_LOGGER_SOCKET_ENV)
def is_available():
"""Returns `True` if shell logger socket available.
:rtype: book
"""
path = _get_socket_path()
if not path:
return False
return os.path.exists(path)
def _get_last_n(n):
with socket.socket(socket.AF_UNIX) as client:
client.connect(_get_socket_path())
request = json.dumps({
"type": "list",
"count": n,
}) + '\n'
client.sendall(request.encode('utf-8'))
response = client.makefile().readline()
return json.loads(response)['commands']
def _get_output_lines(output):
lines = output.split('\n')
screen = pyte.Screen(get_terminal_size().columns, len(lines))
stream = pyte.Stream(screen)
stream.feed('\n'.join(lines))
return screen.display
def get_output(script):
"""Gets command output from shell logger."""
with logs.debug_time(u'Read output from external shell logger'):
commands = _get_last_n(const.SHELL_LOGGER_LIMIT)
for command in commands:
if command['command'] == script:
lines = _get_output_lines(command['output'])
output = '\n'.join(lines).strip()
return output
else:
logs.warn("Output isn't available in shell logger")
return None

View File

@@ -6,8 +6,8 @@ from thefuck.utils import for_app, eager, replace_command
enabled_by_default = apt_available enabled_by_default = apt_available
@for_app('apt', 'apt-get', 'apt-cache')
@sudo_support @sudo_support
@for_app('apt', 'apt-get', 'apt-cache')
def match(command): def match(command):
return 'E: Invalid operation' in command.output return 'E: Invalid operation' in command.output

View File

@@ -3,7 +3,7 @@ import re
from thefuck.utils import for_app, replace_argument from thefuck.utils import for_app, replace_argument
INVALID_CHOICE = "(?<=Invalid choice: ')(.*)(?=', maybe you meant:)" INVALID_CHOICE = "(?<=Invalid choice: ')(.*)(?=', maybe you meant:)"
OPTIONS = "^\s*\*\s(.*)" OPTIONS = "^\\s*\\*\\s(.*)"
@for_app('aws') @for_app('aws')

17
thefuck/rules/az_cli.py Normal file
View File

@@ -0,0 +1,17 @@
import re
from thefuck.utils import for_app, replace_argument
INVALID_CHOICE = "(?=az)(?:.*): '(.*)' is not in the '.*' command group."
OPTIONS = "^The most similar choice to '.*' is:\n\\s*(.*)$"
@for_app('az')
def match(command):
return "is not in the" in command.output and "command group" in command.output
def get_new_command(command):
mistake = re.search(INVALID_CHOICE, command.output).group(1)
options = re.findall(OPTIONS, command.output, flags=re.MULTILINE)
return [replace_argument(command.script, mistake, o) for o in options]

View File

@@ -20,7 +20,7 @@ def _get_formulas():
def _get_similar_formula(formula_name): def _get_similar_formula(formula_name):
return get_closest(formula_name, _get_formulas(), 1, 0.85) return get_closest(formula_name, _get_formulas(), cutoff=0.85)
def match(command): def match(command):

View File

@@ -0,0 +1,19 @@
import re
from thefuck.utils import for_app
warning_regex = re.compile(r'Warning: (?:.(?!is ))+ is already installed and '
r'up-to-date')
message_regex = re.compile(r'To reinstall (?:(?!, ).)+, run `brew reinstall '
r'[^`]+`')
@for_app('brew', at_least=2)
def match(command):
return ('install' in command.script
and warning_regex.search(command.output)
and message_regex.search(command.output))
def get_new_command(command):
return command.script.replace('install', 'reinstall')

14
thefuck/rules/cat_dir.py Normal file
View File

@@ -0,0 +1,14 @@
import os
from thefuck.utils import for_app
@for_app('cat', at_least=1)
def match(command):
return (
command.output.startswith('cat: ') and
os.path.isdir(command.script_parts[1])
)
def get_new_command(command):
return command.script.replace('cat', 'ls', 1)

View File

@@ -2,10 +2,9 @@
import os import os
import six import six
from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir from thefuck.rules import cd_mkdir
from thefuck.utils import for_app from thefuck.utils import for_app, get_close_matches
__author__ = "mmussomele" __author__ = "mmussomele"

View File

@@ -8,8 +8,8 @@ from thefuck.specific.dnf import dnf_available
regex = re.compile(r'No such command: (.*)\.') regex = re.compile(r'No such command: (.*)\.')
@for_app('dnf')
@sudo_support @sudo_support
@for_app('dnf')
def match(command): def match(command):
return 'no such command' in command.output.lower() return 'no such command' in command.output.lower()

View File

@@ -0,0 +1,12 @@
from thefuck.utils import for_app
@for_app('docker')
def match(command):
return ('docker' in command.script
and "access denied" in command.output
and "may require 'docker login'" in command.output)
def get_new_command(command):
return 'docker login && {}'.format(command.script)

View File

@@ -0,0 +1,11 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('commit' in command.script_parts)
@git_support
def get_new_command(command):
return 'git reset HEAD~'

View File

@@ -39,6 +39,6 @@ def get_new_command(command):
while len(command_parts) > push_idx and command_parts[len(command_parts) - 1][0] != '-': while len(command_parts) > push_idx and command_parts[len(command_parts) - 1][0] != '-':
command_parts.pop(len(command_parts) - 1) command_parts.pop(len(command_parts) - 1)
arguments = re.findall(r'git push (.*)', command.output)[0].replace("'", r"\'").strip() arguments = re.findall(r'git push (.*)', command.output)[-1].replace("'", r"\'").strip()
return replace_argument(" ".join(command_parts), 'push', return replace_argument(" ".join(command_parts), 'push',
'push {}'.format(arguments)) 'push {}'.format(arguments))

View File

@@ -1,4 +1,4 @@
from difflib import get_close_matches from thefuck.utils import get_close_matches
from thefuck.specific.git import git_support from thefuck.specific.git import git_support

View File

@@ -8,5 +8,5 @@ def match(command):
def get_new_command(command): def get_new_command(command):
apps = re.findall('([^ ]*) \([^)]*\)', command.output) apps = re.findall('([^ ]*) \\([^)]*\\)', command.output)
return [command.script + ' --app ' + app for app in apps] return [command.script + ' --app ' + app for app in apps]

View File

@@ -1,5 +1,5 @@
from difflib import get_close_matches from thefuck.utils import get_close_matches, get_closest, \
from thefuck.utils import get_closest, get_valid_history_without_current get_valid_history_without_current
def match(command): def match(command):

View File

@@ -0,0 +1,27 @@
from thefuck.utils import replace_argument
import re
# regex to match a suggested help command from the tool output
help_regex = r"(?:Run|Try) '([^']+)'(?: or '[^']+')? for (?:details|more information)."
def match(command):
if re.search(help_regex, command.output, re.I) is not None:
return True
if '--help' in command.output:
return True
return False
def get_new_command(command):
if re.search(help_regex, command.output) is not None:
match_obj = re.search(help_regex, command.output, re.I)
return match_obj.group(1)
return replace_argument(command.script, '-h', '--help')
enabled_by_default = True
priority = 5000

View File

@@ -1,5 +1,4 @@
from thefuck.utils import replace_command, for_app from thefuck.utils import for_app, get_close_matches, replace_command
from difflib import get_close_matches
import re import re
@@ -25,8 +24,7 @@ def get_new_command(command):
available_lifecycles = _getavailable_lifecycles(command) available_lifecycles = _getavailable_lifecycles(command)
if available_lifecycles and failed_lifecycle: if available_lifecycles and failed_lifecycle:
selected_lifecycle = get_close_matches( selected_lifecycle = get_close_matches(
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "), failed_lifecycle.group(1), available_lifecycles.group(1).split(", "))
3, 0.6)
return replace_command(command, failed_lifecycle.group(1), selected_lifecycle) return replace_command(command, failed_lifecycle.group(1), selected_lifecycle)
else: else:
return [] return []

View File

@@ -1,5 +1,4 @@
from difflib import get_close_matches from thefuck.utils import get_all_executables, get_close_matches, \
from thefuck.utils import get_all_executables, \
get_valid_history_without_current, get_closest, which get_valid_history_without_current, get_closest, which
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
@@ -7,7 +6,8 @@ from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
def match(command): def match(command):
return (not which(command.script_parts[0]) return (not which(command.script_parts[0])
and 'not found' in command.output and ('not found' in command.output
or 'is not recognized as' in command.output)
and bool(get_close_matches(command.script_parts[0], and bool(get_close_matches(command.script_parts[0],
get_all_executables()))) get_all_executables())))

View File

@@ -1,9 +1,9 @@
""" Fixes wrong package names with pacman or yaourt. """ Fixes wrong package names with pacman or yaourt.
For example the `llc` program is in package `llvm` so this: For example the `llc` program is in package `llvm` so this:
yaourt -S llc yay -S llc
should be: should be:
yaourt -S llvm yay -S llvm
""" """
from thefuck.utils import replace_command from thefuck.utils import replace_command
@@ -12,7 +12,7 @@ from thefuck.specific.archlinux import get_pkgfile, archlinux_env
def match(command): def match(command):
return (command.script_parts return (command.script_parts
and (command.script_parts[0] in ('pacman', 'yaourt') and (command.script_parts[0] in ('pacman', 'yay', 'yaourt')
or command.script_parts[0:2] == ['sudo', 'pacman']) or command.script_parts[0:2] == ['sudo', 'pacman'])
and 'error: target not found:' in command.output) and 'error: target not found:' in command.output)

View File

@@ -0,0 +1,15 @@
from thefuck.utils import for_app
from thefuck.specific.sudo import sudo_support
@sudo_support
@for_app('pip')
def match(command):
return ('pip install' in command.script and 'Permission denied' in command.output)
def get_new_command(command):
if '--user' not in command.script: # add --user (attempt 1)
return command.script.replace(' install ', ' install --user ')
return 'sudo {}'.format(command.script.replace(' --user', '')) # since --user didn't fix things, let's try sudo (attempt 2)

View File

@@ -0,0 +1,33 @@
import re
from subprocess import PIPE, Popen
from thefuck.utils import (cache, for_app, replace_argument, replace_command,
which)
COMMON_TYPOS = {
'list': ['versions', 'install --list'],
'remove': ['uninstall'],
}
@for_app('pyenv')
def match(command):
return 'pyenv: no such command' in command.output
def get_pyenv_commands():
proc = Popen(['pyenv', 'commands'], stdout=PIPE)
return [line.decode('utf-8').strip() for line in proc.stdout.readlines()]
if which('pyenv'):
get_pyenv_commands = cache(which('pyenv'))(get_pyenv_commands)
@for_app('pyenv')
def get_new_command(command):
broken = re.findall(r"pyenv: no such command `([^']*)'", command.output)[0]
matched = [replace_argument(command.script, broken, common_typo)
for common_typo in COMMON_TYPOS.get(broken, [])]
matched.extend(replace_command(command, broken, get_pyenv_commands()))
return matched

View File

@@ -2,20 +2,48 @@
from thefuck.utils import memoize, get_alias from thefuck.utils import memoize, get_alias
target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?''' target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''
# any new keyboard layout must be appended
greek = u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?'''
source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''', source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''',
u'''йцукенгшщзхїфівапролджєячсмитьбю.ЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЄЯЧСМИТЬБЮ,''',
u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''', u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''',
u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?''', u'''/'קראטוןםפ][שדגכעיחלךף,זסבהנמצתץ.QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?''',
u'''/'קראטוןםפ][שדגכעיחלךף,זסבהנמצתץ.QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''] greek]
source_to_target = {
greek: {u';': "q", u'ς': "w", u'ε': "e", u'ρ': "r", u'τ': "t", u'υ': "y",
u'θ': "u", u'ι': "i", u'ο': "o", u'π': "p", u'[': "[", u']': "]",
u'α': "a", u'σ': "s", u'δ': "d", u'φ': "f", u'γ': "g", u'η': "h",
u'ξ': "j", u'κ': "k", u'λ': "l", u'΄': "'", u'ζ': "z", u'χ': "x",
u'ψ': "c", u'ω': "v", u'β': "b", u'ν': "n", u'μ': "m", u',': ",",
u'.': ".", u'/': "/", u':': "Q", u'΅': "W", u'Ε': "E", u'Ρ': "R",
u'Τ': "T", u'Υ': "Y", u'Θ': "U", u'Ι': "I", u'Ο': "O", u'Π': "P",
u'{': "{", u'}': "}", u'Α': "A", u'Σ': "S", u'Δ': "D", u'Φ': "F",
u'Γ': "G", u'Η': "H", u'Ξ': "J", u'Κ': "K", u'Λ': "L", u'¨': ":",
u'"': '"', u'Ζ': "Z", u'Χ': "X", u'Ψ': "C", u'Ω': "V", u'Β': "B",
u'Ν': "N", u'Μ': "M", u'<': "<", u'>': ">", u'?': "?", u'ά': "a",
u'έ': "e", u'ύ': "y", u'ί': "i", u'ό': "o", u'ή': 'h', u'ώ': u"v",
u'Ά': "A", u'Έ': "E", u'Ύ': "Y", u'Ί': "I", u'Ό': "O", u'Ή': "H",
u'Ώ': "V"},
}
@memoize @memoize
def _get_matched_layout(command): def _get_matched_layout(command):
# don't use command.split_script here because a layout mismatch will likely # don't use command.split_script here because a layout mismatch will likely
# result in a non-splitable sript as per shlex # result in a non-splitable script as per shlex
cmd = command.script.split(' ') cmd = command.script.split(' ')
for source_layout in source_layouts: for source_layout in source_layouts:
if all([ch in source_layout or ch in '-_' for ch in cmd[0]]): is_all_match = True
for cmd_part in cmd:
if not all([ch in source_layout or ch in '-_' for ch in cmd_part]):
is_all_match = False
break
if is_all_match:
return source_layout return source_layout
@@ -27,12 +55,18 @@ def _switch(ch, layout):
def _switch_command(command, layout): def _switch_command(command, layout):
# Layouts with different amount of characters than English
if layout in source_to_target:
return ''.join(source_to_target[layout].get(ch, ch)
for ch in command.script)
return ''.join(_switch(ch, layout) for ch in command.script) return ''.join(_switch(ch, layout) for ch in command.script)
def match(command): def match(command):
if 'not found' not in command.output: if 'not found' not in command.output:
return False return False
matched_layout = _get_matched_layout(command) matched_layout = _get_matched_layout(command)
return (matched_layout and return (matched_layout and
_switch_command(command, matched_layout) != get_alias()) _switch_command(command, matched_layout) != get_alias())

View File

@@ -9,5 +9,6 @@ def match(command):
def get_new_command(command): def get_new_command(command):
path = re.findall(r"touch: cannot touch '(.+)/.+':", command.output)[0] path = path = re.findall(
r"touch: (?:cannot touch ')?(.+)/.+'?:", command.output)[0]
return shell.and_(u'mkdir -p {}'.format(path), command.script) return shell.and_(u'mkdir -p {}'.format(path), command.script)

View File

@@ -16,7 +16,8 @@ shells = {'bash': Bash,
'zsh': Zsh, 'zsh': Zsh,
'csh': Tcsh, 'csh': Tcsh,
'tcsh': Tcsh, 'tcsh': Tcsh,
'powershell': Powershell} 'powershell': Powershell,
'pwsh': Powershell}
def _get_shell_from_env(): def _get_shell_from_env():

View File

@@ -1,13 +1,16 @@
import os import os
from subprocess import Popen, PIPE
from tempfile import gettempdir from tempfile import gettempdir
from uuid import uuid4 from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import DEVNULL, memoize
from .generic import Generic from .generic import Generic
class Bash(Generic): class Bash(Generic):
friendly_name = 'Bash'
def app_alias(self, alias_name): def app_alias(self, alias_name):
# It is VERY important to have the variables declared WITHIN the function # It is VERY important to have the variables declared WITHIN the function
return ''' return '''
@@ -19,8 +22,8 @@ class Bash(Generic):
export TF_HISTORY=$(fc -ln -10); export TF_HISTORY=$(fc -ln -10);
export PYTHONIOENCODING=utf-8; export PYTHONIOENCODING=utf-8;
TF_CMD=$( TF_CMD=$(
thefuck {argument_placeholder} $@ thefuck {argument_placeholder} "$@"
) && eval $TF_CMD; ) && eval "$TF_CMD";
unset TF_HISTORY; unset TF_HISTORY;
export PYTHONIOENCODING=$TF_PYTHONIOENCODING; export PYTHONIOENCODING=$TF_PYTHONIOENCODING;
{alter_history} {alter_history}
@@ -78,6 +81,12 @@ class Bash(Generic):
config = 'bash config' config = 'bash config'
return self._create_shell_configuration( return self._create_shell_configuration(
content=u'eval $(thefuck --alias)', content=u'eval "$(thefuck --alias)"',
path=config, path=config,
reload=u'source {}'.format(config)) reload=u'source {}'.format(config))
def _get_version(self):
"""Returns the version of the current shell"""
proc = Popen(['bash', '-c', 'echo $BASH_VERSION'],
stdout=PIPE, stderr=DEVNULL)
return proc.stdout.read().decode('utf-8').strip()

View File

@@ -5,6 +5,7 @@ import sys
import six import six
from .. import logs from .. import logs
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER
from ..utils import DEVNULL, cache from ..utils import DEVNULL, cache
from .generic import Generic from .generic import Generic
@@ -20,22 +21,32 @@ def _get_functions(overridden):
def _get_aliases(overridden): def _get_aliases(overridden):
aliases = {} aliases = {}
proc = Popen(['fish', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) proc = Popen(['fish', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
alias_out = proc.stdout.read().decode('utf-8').strip().split('\n') alias_out = proc.stdout.read().decode('utf-8').strip()
for alias in alias_out: if not alias_out:
name, value = alias.replace('alias ', '', 1).split(' ', 1) return aliases
for alias in alias_out.split('\n'):
for separator in (' ', '='):
split_alias = alias.replace('alias ', '', 1).split(separator, 1)
if len(split_alias) == 2:
name, value = split_alias
break
else:
continue
if name not in overridden: if name not in overridden:
aliases[name] = value aliases[name] = value
return aliases return aliases
class Fish(Generic): class Fish(Generic):
friendly_name = 'Fish Shell'
def _get_overridden_aliases(self): def _get_overridden_aliases(self):
overridden = os.environ.get('THEFUCK_OVERRIDDEN_ALIASES', overridden = os.environ.get('THEFUCK_OVERRIDDEN_ALIASES',
os.environ.get('TF_OVERRIDDEN_ALIASES', '')) os.environ.get('TF_OVERRIDDEN_ALIASES', ''))
default = {'cd', 'grep', 'ls', 'man', 'open'} default = {'cd', 'grep', 'ls', 'man', 'open'}
for alias in overridden.split(','): for alias in overridden.split(','):
default.add(alias.strip()) default.add(alias.strip())
return default return sorted(default)
def app_alias(self, alias_name): def app_alias(self, alias_name):
if settings.alter_history: if settings.alter_history:
@@ -48,11 +59,11 @@ class Fish(Generic):
return ('function {0} -d "Correct your previous console command"\n' return ('function {0} -d "Correct your previous console command"\n'
' set -l fucked_up_command $history[1]\n' ' set -l fucked_up_command $history[1]\n'
' env TF_SHELL=fish TF_ALIAS={0} PYTHONIOENCODING=utf-8' ' env TF_SHELL=fish TF_ALIAS={0} PYTHONIOENCODING=utf-8'
' thefuck $fucked_up_command | read -l unfucked_command\n' ' thefuck $fucked_up_command {2} $argv | read -l unfucked_command\n'
' if [ "$unfucked_command" != "" ]\n' ' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n{1}' ' eval $unfucked_command\n{1}'
' end\n' ' end\n'
'end').format(alias_name, alter_history) 'end').format(alias_name, alter_history, ARGUMENT_PLACEHOLDER)
def get_aliases(self): def get_aliases(self):
overridden = self._get_overridden_aliases() overridden = self._get_overridden_aliases()
@@ -95,6 +106,11 @@ class Fish(Generic):
path='~/.config/fish/config.fish', path='~/.config/fish/config.fish',
reload='fish') reload='fish')
def _get_version(self):
"""Returns the version of the current shell"""
proc = Popen(['fish', '--version'], stdout=PIPE, stderr=DEVNULL)
return proc.stdout.read().decode('utf-8').split()[-1]
def put_to_history(self, command): def put_to_history(self, command):
try: try:
return self._put_to_history(command) return self._put_to_history(command)

View File

@@ -14,6 +14,8 @@ ShellConfiguration = namedtuple('ShellConfiguration', (
class Generic(object): class Generic(object):
friendly_name = 'Generic Shell'
def get_aliases(self): def get_aliases(self):
return {} return {}
@@ -34,8 +36,8 @@ class Generic(object):
return command_script return command_script
def app_alias(self, alias_name): 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(alias_name) """thefuck "$(fc -ln -1)")"'""".format(alias_name)
def instant_mode_alias(self, alias_name): def instant_mode_alias(self, alias_name):
warn("Instant mode not supported by your shell") warn("Instant mode not supported by your shell")
@@ -82,7 +84,7 @@ class Generic(object):
encoded = self.encode_utf8(command) encoded = self.encode_utf8(command)
try: try:
splitted = [s.replace("??", "\ ") for s in shlex.split(encoded.replace('\ ', '??'))] splitted = [s.replace("??", "\\ ") for s in shlex.split(encoded.replace('\\ ', '??'))]
except ValueError: except ValueError:
splitted = encoded.split(' ') splitted = encoded.split(' ')
@@ -131,6 +133,19 @@ class Generic(object):
'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset', 'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset',
'until', 'wait', 'while'] 'until', 'wait', 'while']
def _get_version(self):
"""Returns the version of the current shell"""
return ''
def info(self):
"""Returns the name and version of the current shell"""
try:
version = self._get_version()
except Exception as e:
warn(u'Could not determine shell version: {}'.format(e))
version = ''
return u'{} {}'.format(self.friendly_name, version).rstrip()
def _create_shell_configuration(self, content, path, reload): def _create_shell_configuration(self, content, path, reload):
return ShellConfiguration( return ShellConfiguration(
content=content, content=content,

View File

@@ -1,7 +1,11 @@
from subprocess import Popen, PIPE
from ..utils import DEVNULL
from .generic import Generic, ShellConfiguration from .generic import Generic, ShellConfiguration
class Powershell(Generic): class Powershell(Generic):
friendly_name = 'PowerShell'
def app_alias(self, alias_name): def app_alias(self, alias_name):
return 'function ' + alias_name + ' {\n' \ return 'function ' + alias_name + ' {\n' \
' $history = (Get-History -Count 1).CommandLine;\n' \ ' $history = (Get-History -Count 1).CommandLine;\n' \
@@ -12,6 +16,7 @@ class Powershell(Generic):
' else { iex "$fuck"; }\n' \ ' else { iex "$fuck"; }\n' \
' }\n' \ ' }\n' \
' }\n' \ ' }\n' \
' [Console]::ResetColor() \n' \
'}\n' '}\n'
def and_(self, *commands): def and_(self, *commands):
@@ -23,3 +28,16 @@ class Powershell(Generic):
path='$profile', path='$profile',
reload='& $profile', reload='& $profile',
can_configure_automatically=False) can_configure_automatically=False)
def _get_version(self):
"""Returns the version of the current shell"""
try:
proc = Popen(
['powershell.exe', '$PSVersionTable.PSVersion'],
stdout=PIPE,
stderr=DEVNULL)
version = proc.stdout.read().decode('utf-8').rstrip().split('\n')
return '.'.join(version[-1].split())
except IOError:
proc = Popen(['pwsh', '--version'], stdout=PIPE, stderr=DEVNULL)
return proc.stdout.read().decode('utf-8').split()[-1]

View File

@@ -6,6 +6,8 @@ from .generic import Generic
class Tcsh(Generic): class Tcsh(Generic):
friendly_name = 'Tcsh'
def app_alias(self, alias_name): def app_alias(self, alias_name):
return ("alias {0} 'setenv TF_SHELL tcsh && setenv TF_ALIAS {0} && " return ("alias {0} 'setenv TF_SHELL tcsh && setenv TF_ALIAS {0} && "
"set fucked_cmd=`history -h 2 | head -n 1` && " "set fucked_cmd=`history -h 2 | head -n 1` && "
@@ -35,3 +37,8 @@ class Tcsh(Generic):
content=u'eval `thefuck --alias`', content=u'eval `thefuck --alias`',
path='~/.tcshrc', path='~/.tcshrc',
reload='tcsh') reload='tcsh')
def _get_version(self):
"""Returns the version of the current shell"""
proc = Popen(['tcsh', '--version'], stdout=PIPE, stderr=DEVNULL)
return proc.stdout.read().decode('utf-8').split()[1]

View File

@@ -1,14 +1,17 @@
from time import time from time import time
import os import os
from subprocess import Popen, PIPE
from tempfile import gettempdir from tempfile import gettempdir
from uuid import uuid4 from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import DEVNULL, memoize
from .generic import Generic from .generic import Generic
class Zsh(Generic): class Zsh(Generic):
friendly_name = 'ZSH'
def app_alias(self, alias_name): def app_alias(self, alias_name):
# It is VERY important to have the variables declared WITHIN the function # It is VERY important to have the variables declared WITHIN the function
return ''' return '''
@@ -16,8 +19,10 @@ class Zsh(Generic):
TF_PYTHONIOENCODING=$PYTHONIOENCODING; TF_PYTHONIOENCODING=$PYTHONIOENCODING;
export TF_SHELL=zsh; export TF_SHELL=zsh;
export TF_ALIAS={name}; export TF_ALIAS={name};
export TF_SHELL_ALIASES=$(alias); TF_SHELL_ALIASES=$(alias);
export TF_HISTORY="$(fc -ln -10)"; export TF_SHELL_ALIASES;
TF_HISTORY="$(fc -ln -10)";
export TF_HISTORY;
export PYTHONIOENCODING=utf-8; export PYTHONIOENCODING=utf-8;
TF_CMD=$( TF_CMD=$(
thefuck {argument_placeholder} $@ thefuck {argument_placeholder} $@
@@ -49,7 +54,7 @@ class Zsh(Generic):
export THEFUCK_INSTANT_MODE=True; export THEFUCK_INSTANT_MODE=True;
export THEFUCK_OUTPUT_LOG={log}; export THEFUCK_OUTPUT_LOG={log};
thefuck --shell-logger {log}; thefuck --shell-logger {log};
rm {log}; rm -f {log};
exit exit
'''.format(log=log_path) '''.format(log=log_path)
@@ -83,3 +88,9 @@ class Zsh(Generic):
content=u'eval $(thefuck --alias)', content=u'eval $(thefuck --alias)',
path='~/.zshrc', path='~/.zshrc',
reload='source ~/.zshrc') reload='source ~/.zshrc')
def _get_version(self):
"""Returns the version of the current shell"""
proc = Popen(['zsh', '-c', 'echo $ZSH_VERSION'],
stdout=PIPE, stderr=DEVNULL)
return proc.stdout.read().decode('utf-8').strip()

View File

@@ -32,7 +32,9 @@ def get_pkgfile(command):
def archlinux_env(): def archlinux_env():
if utils.which('yaourt'): if utils.which('yay'):
pacman = 'yay'
elif utils.which('yaourt'):
pacman = 'yaourt' pacman = 'yaourt'
elif utils.which('pacman'): elif utils.which('pacman'):
pacman = 'sudo pacman' pacman = 'sudo pacman'

View File

@@ -1,5 +1,4 @@
import os import os
import sys
import msvcrt import msvcrt
import win_unicode_console import win_unicode_console
from .. import const from .. import const
@@ -12,20 +11,18 @@ def init_output():
def get_key(): def get_key():
ch = msvcrt.getch() ch = msvcrt.getwch()
if ch in (b'\x00', b'\xe0'): # arrow or function key prefix? if ch in ('\x00', '\xe0'): # arrow or function key prefix?
ch = msvcrt.getch() # second call returns the actual key code ch = msvcrt.getwch() # second call returns the actual key code
if ch in const.KEY_MAPPING: if ch in const.KEY_MAPPING:
return const.KEY_MAPPING[ch] return const.KEY_MAPPING[ch]
if ch == b'H': if ch == 'H':
return const.KEY_UP return const.KEY_UP
if ch == b'P': if ch == 'P':
return const.KEY_DOWN return const.KEY_DOWN
encoding = (sys.stdout.encoding return ch
or os.environ.get('PYTHONIOENCODING', 'utf-8'))
return ch.decode(encoding)
def open_command(arg): def open_command(arg):

View File

@@ -52,7 +52,7 @@ class CommandSelector(object):
@property @property
def value(self): def value(self):
""":rtype hefuck.types.CorrectedCommand""" """:rtype thefuck.types.CorrectedCommand"""
return self._commands[self._index] return self._commands[self._index]

View File

@@ -3,11 +3,12 @@ import os
import pickle import pickle
import re import re
import shelve import shelve
import sys
import six import six
from decorator import decorator from decorator import decorator
from difflib import get_close_matches from difflib import get_close_matches as difflib_get_close_matches
from functools import wraps from functools import wraps
from .logs import warn from .logs import warn, exception
from .conf import settings from .conf import settings
from .system import Path from .system import Path
@@ -86,16 +87,23 @@ def default_settings(params):
return decorator(_default_settings) return decorator(_default_settings)
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True): def get_closest(word, possibilities, cutoff=0.6, fallback_to_first=True):
"""Returns closest match or just first from possibilities.""" """Returns closest match or just first from possibilities."""
possibilities = list(possibilities) possibilities = list(possibilities)
try: try:
return get_close_matches(word, possibilities, n, cutoff)[0] return difflib_get_close_matches(word, possibilities, 1, cutoff)[0]
except IndexError: except IndexError:
if fallback_to_first: if fallback_to_first:
return possibilities[0] return possibilities[0]
def get_close_matches(word, possibilities, n=None, cutoff=0.6):
"""Overrides `difflib.get_close_match` to controle argument `n`."""
if n is None:
n = settings.num_close_matches
return difflib_get_close_matches(word, possibilities, n, cutoff)
@memoize @memoize
def get_all_executables(): def get_all_executables():
from thefuck.shells import shell from thefuck.shells import shell
@@ -110,7 +118,7 @@ def get_all_executables():
tf_entry_points = ['thefuck', 'fuck'] tf_entry_points = ['thefuck', 'fuck']
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(os.pathsep)
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]
@@ -190,6 +198,13 @@ class Cache(object):
self._db = None self._db = None
def _init_db(self): def _init_db(self):
try:
self._setup_db()
except Exception:
exception("Unable to init cache", sys.exc_info())
self._db = {}
def _setup_db(self):
cache_dir = self._get_cache_dir() cache_dir = self._get_cache_dir()
cache_path = Path(cache_dir).joinpath('thefuck').as_posix() cache_path = Path(cache_dir).joinpath('thefuck').as_posix()

View File

@@ -1,10 +1,10 @@
[tox] [tox]
envlist = py27,py34,py35,py36 envlist = py27,py34,py35,py36,py37
[testenv] [testenv]
deps = -rrequirements.txt deps = -rrequirements.txt
commands = py.test -v --capture=sys commands = py.test -v --capture=sys
[flake8] [flake8]
ignore = E501,W503 ignore = E501,W503,W504
exclude = venv,build,.tox,setup.py,fastentrypoints.py exclude = venv,build,.tox,setup.py,fastentrypoints.py