1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 23:51:59 +00:00

Compare commits

..

107 Commits
3.3 ... 3.9

Author SHA1 Message Date
nvbn
f74bbb7a9a Bump to 3.9 2016-04-24 17:56:06 +03:00
nvbn
d6c2c7266d Merge branch 'scorphus-fish-put-to-history' 2016-04-22 03:17:40 +03:00
nvbn
51839e65cd #495: Add comment in put_to_history 2016-04-22 03:16:16 +03:00
nvbn
d5ae3a6b41 Merge branch 'fish-put-to-history' of https://github.com/scorphus/thefuck into scorphus-fish-put-to-history
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2016-04-22 03:14:31 +03:00
Vladimir Iakovlev
9f421a17e5 Merge pull request #494 from scorphus/brew-update-formula
#N/A Add a new rule `brew_update_formula`
2016-04-21 13:06:11 +03:00
Pablo Santiago Blum de Aguiar
9d9820676a #N/A Add a new rule brew_update_formula 2016-04-20 22:27:39 -03:00
nvbn
5ec4909d2f #N/A: Minor style changes 2016-04-12 00:37:18 +03:00
nvbn
c6d2766553 #N/A: Add chmod +x rule 2016-04-11 16:13:41 +03:00
nvbn
c6af8409d9 Bump to 3.8 2016-04-08 14:36:53 +03:00
Vladimir Iakovlev
95e7d00aec Merge pull request #492 from scorphus/overridden-aliases
Treat overridden aliases in a better way
2016-04-07 13:09:08 +03:00
Pablo Santiago Blum de Aguiar
cdccf1881e #253: Use a better name for that env var 2016-04-06 23:11:19 -03:00
Pablo Santiago Blum de Aguiar
db6053b301 #253: Update default overridden aliases with user's 2016-04-06 22:58:08 -03:00
nvbn
183b70c8b8 Merge branch 'scorphus-git-branch-exists' 2016-04-03 14:08:26 +03:00
nvbn
5e0cc8c703 #491: yield possible fixes in git_branch_exists rule 2016-04-03 14:07:39 +03:00
nvbn
1aa2ec1795 Merge branch 'git-branch-exists' of https://github.com/scorphus/thefuck into scorphus-git-branch-exists 2016-04-03 14:02:32 +03:00
Pablo Santiago Blum de Aguiar
0c98053f74 #N/A Add a new rule git_branch_exists 2016-04-03 00:09:15 -03:00
Vladimir Iakovlev
17b2fba48d Merge pull request #489 from scorphus/improve-readme
Improve readme
2016-04-02 01:44:51 +03:00
Pablo Santiago Blum de Aguiar
43886c38ff #N/A Add more fancy badges 2016-03-31 22:07:10 -03:00
Pablo Santiago Blum de Aguiar
9070748a86 #N/A Use reference links 2016-03-31 22:06:01 -03:00
Pablo Santiago Blum de Aguiar
61de6f4a51 #N/A Reformat parts of README.md 2016-03-31 15:53:32 -03:00
Vladimir Iakovlev
d102af41d9 #488 Add AppVeyor badge 2016-03-31 04:50:40 +03:00
Vladimir Iakovlev
b7002bb9f9 Merge pull request #488 from scorphus/app-veyor
App veyor
2016-03-31 04:44:44 +03:00
Pablo Santiago Blum de Aguiar
18b4f5df6a #486: Run tests on AppVeyor 2016-03-29 23:39:53 -03:00
Pablo Santiago Blum de Aguiar
28153db4a8 #486: Ignore a test on Windows 2016-03-29 23:39:53 -03:00
Pablo Santiago Blum de Aguiar
047a1a6072 #486: Use Path instead of PosixPath 2016-03-29 23:39:53 -03:00
Pablo Santiago Blum de Aguiar
69db5c70e6 #486: Fix path joining on Windows 2016-03-29 23:39:45 -03:00
nvbn
fa1edd4bae Bump to 3.7 2016-03-23 05:12:29 +02:00
Vladimir Iakovlev
333c4b2a3f Merge pull request #483 from shakaran/patch-1
Update install procedure for pip
2016-03-23 05:11:07 +02:00
Vladimir Iakovlev
b1f10642fa Merge pull request #487 from scorphus/stdout-encoding-none
#486: Use alternative encoding when sys.stdout.encoding is None
2016-03-23 05:09:36 +02:00
Pablo Santiago Blum de Aguiar
047efd5575 #486: Use alternative encoding when sys.stdout.encoding is None
Fix #486
2016-03-22 16:13:59 -03:00
Vladimir Iakovlev
f604756cb7 Merge pull request #485 from scorphus/484-stdin-pipe
#484: Use PIPE as stdin when Popening the script
2016-03-21 22:35:18 +02:00
Pablo Santiago Blum de Aguiar
a27115bff1 #484: Use PIPE as stdin when Popening the script
Fix #484
2016-03-21 16:59:35 -03:00
Ángel Guzmán Maeso
5d00b3bc25 Update install procedure for pip
Avoid the warning:

The directory '/home/someuser/.cache/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
2016-03-21 17:48:41 +01:00
Vladimir Iakovlev
0cf4f5e8b0 Merge pull request #481 from scorphus/fix-git-add
Fix `git_add` rule
2016-03-19 12:59:22 +02:00
Pablo Santiago Blum de Aguiar
41707b80c6 #N/A: Fix git_add rule 2016-03-18 22:46:38 -03:00
Vladimir Iakovlev
3a39deb485 Merge pull request #478 from MattKotsenas/feature/powershell-shell
Add Powershell as shell
2016-03-19 02:45:26 +02:00
Matt Kotsenas
d4bc8cebf1 Replace raise with return for Ctrl+C in Windows
- Replace the raise `const.CtrlC` with `return const.CtrlC` the match the
  unix implementation and prevent a stacktrace when cancelling a command
  on Windows
2016-03-16 16:37:59 -07:00
Matt Kotsenas
6daf687237 Add Powershell as a supported shell
- There may be additional functionality to implement, but I've been
  running this way for a month with no known issues
2016-03-16 16:06:09 -07:00
Matt Kotsenas
2207dd2668 Update _get_shell to work with Windows
- _get_shell assumed the parent process would always be the shell process, in Powershell the
  parent process is Python, with the grandparent being the shell
- Switched to walking the process tree so the same code path can be used in both places
2016-03-15 14:10:04 -07:00
Vladimir Iakovlev
d1ab08a797 Merge pull request #477 from scorphus/git-rm-recursive
#N/A: Add new `git_rm_recursive` rule
2016-03-15 04:30:31 +03:00
Pablo Santiago Blum de Aguiar
51e89a36ef #N/A: Add new git_rm_recursive rule 2016-03-14 18:59:32 -03:00
nvbn
39f7cc37eb Bump to 3.6 2016-03-13 17:46:14 +03:00
nvbn
251b69b5a0 #475: Try to use already used executable in no_command 2016-03-13 15:10:37 +03:00
nvbn
8b1f078e27 Bump to 3.5 2016-03-13 03:14:18 +03:00
Vladimir Iakovlev
a47e84fa6b Merge pull request #476 from scorphus/git-help-aliased
#N/A: Add new `git_help_aliased` rule
2016-03-13 01:24:57 +03:00
Pablo Santiago Blum de Aguiar
bcab700215 #N/A: Add new git_help_aliased rule 2016-03-12 18:51:47 -03:00
Vladimir Iakovlev
4fb99fd7a8 Merge pull request #474 from scorphus/alias-variables
#301: Set variables within the alias
2016-03-10 13:25:50 +03:00
Pablo Santiago Blum de Aguiar
bb5f6bb705 #301: Set variables within the alias
Fix #301
2016-03-09 21:58:18 -03:00
Vladimir Iakovlev
d92765d5df Merge pull request #473 from scorphus/gdbm-unavailable
#439 & #447: Remove cache if created with unavailable db
2016-03-10 02:52:33 +03:00
Pablo Santiago Blum de Aguiar
d8de5cfd20 #439 & #447: Remove cache if created with unavailable db
When switching between Python versions, the database package used to
create the cache might be unavailable and an ImportError is raised,
such as `ImportError: No module named gdbm`.
2016-03-08 14:38:16 -03:00
Vladimir Iakovlev
d73b14ce4b Merge pull request #472 from PLNech/master
Rules: git remote add instead of set-url if remote does not exist
2016-03-06 13:33:14 +03:00
Paul-Louis NECH
04b83cf7e8 Rules: git remote add instead of set-url if remote does not exist
Tests: Added test for git_remote_seturl_add

Rules: renamed new rule with a more appropriate name

README: added new rule

Style: Formatting

New rule: corrected test name

Developed tests
2016-03-04 22:35:55 +01:00
nvbn
019f0abf67 Bump to 3.4 2016-03-04 00:21:51 +03:00
nvbn
c8d3dcd1bf Merge branch 'lawrencebenson-master' 2016-03-04 00:20:43 +03:00
nvbn
4f5b382df4 #471: Use parametrized tests 2016-03-04 00:20:33 +03:00
lawrencebenson
77ad68b04b Fix encoding error in source file example 2016-03-03 10:43:33 +01:00
lawrencebenson
6b0311181d Fix encoding error for Python 2 2016-03-02 23:19:22 +01:00
lawrencebenson
48808f93ac Add no hard link support for ln 2016-03-02 17:57:53 +01:00
nvbn
7ce4307c87 #402: Don't invoke bash for getting aliases 2016-03-01 01:28:21 +03:00
nvbn
f7f0660114 #402: Don't invoke zsh for getting aliases 2016-03-01 01:21:51 +03:00
nvbn
46f2351907 #N/A: zsh history doesn't update on travis-ci without manual fc -r call 2016-02-22 18:47:15 +03:00
nvbn
e9de01fa41 #N/A: Move all consts to const 2016-02-22 18:40:28 +03:00
nvbn
9b260eb239 ⚠️ #442: Change history only on shell side 2016-02-22 18:31:28 +03:00
Vladimir Iakovlev
f20d4dbf85 Merge pull request #465 from mcarton/histfile2
Let bash handle bash’s history
2016-02-22 17:31:59 +03:00
mcarton
4af7dc2748 Let bash handle bash’s history 2016-02-22 14:53:53 +01:00
Vladimir Iakovlev
c8e9606c7d Merge pull request #462 from scorphus/fish-alias
#N/A Remove fucked up cmd from history regardless of status
2016-02-22 15:48:06 +03:00
Pablo Santiago Blum de Aguiar
d71b9c2e62 #N/A Remove fucked up cmd from history regardless of status
Most fucked up commands are erroneous, but that's not always the case.
2016-02-18 15:17:46 -02:00
Vladimir Iakovlev
60497b9d04 Merge pull request #461 from mcarton/ruby-gem
Add ruby gem error message to sudo patterns
2016-02-18 12:21:54 +03:00
mcarton
619af2638a Make sudo patterns lowercase 2016-02-16 22:27:01 +01:00
mcarton
a1f15bfe5f Add ruby gem error message to sudo patterns 2016-02-16 22:26:22 +01:00
Vladimir Iakovlev
ce959b2a8b Merge pull request #458 from scorphus/anydbm
#439 #447 Except anydbm instead of gdbm for PY2
2016-02-15 14:21:37 +03:00
Pablo Santiago Blum de Aguiar
3cc4940842 #439 #447 Except anydbm instead of gdbm for PY2
Fix #439 and close #447
2016-02-14 00:43:07 -02:00
nvbn
470c0ef699 #444: Check pip version in setup.py 2016-02-12 20:11:08 +03:00
nvbn
4f95b3365a #N/A: Add grep_arguments_order rule 2016-02-08 11:04:19 +03:00
nvbn
bbfd53d718 #N/A: Use shutil.which when possible 2016-02-06 16:42:42 +03:00
nvbn
28d078708b #N/A: Check that brew/apt available in related rules 2016-02-06 16:37:11 +03:00
nvbn
778d5f3e6e #N/A: Add npm_wrong_command rule 2016-02-06 15:18:44 +03:00
Vladimir Iakovlev
c5acee54ea Merge pull request #452 from scorphus/increase-coverage
Increase coverage
2016-02-01 17:52:08 +03:00
Pablo Santiago Blum de Aguiar
c3e9c1bfc1 #N/A Completely test rules.apt_get 2016-01-31 19:55:45 -02:00
Pablo Santiago Blum de Aguiar
6c25b33b9e #N/A Add parameter and skip a test for apt_get rule 2016-01-31 19:55:45 -02:00
Pablo Santiago Blum de Aguiar
052f415d94 #N/A Completely test rules.gulp_not_task 2016-01-31 19:55:44 -02:00
Pablo Santiago Blum de Aguiar
3438d6dde7 #N/A Completely test rules.git_checkout 2016-01-31 19:55:44 -02:00
Vladimir Iakovlev
af0fe66a9f Merge pull request #441 from nvbn/simplify-shells
Split `shells` module
2016-01-31 00:04:35 +03:00
nvbn
fe07fcaa62 #441: Remove shells methods wrappers 2016-01-29 13:09:40 +03:00
nvbn
b5dc7aab6d #441: Remove all logic from shells methods wrappers 2016-01-29 12:30:31 +03:00
nvbn
a2ec5aa3ff #441: Move function for getting current alias to utils 2016-01-29 12:22:31 +03:00
nvbn
0b2bda9e85 Merge branch 'master' into simplify-shells 2016-01-29 12:15:05 +03:00
nvbn
68c882cf1a Merge branch 'fix-fish-how-to' of https://github.com/scorphus/thefuck into scorphus-fix-fish-how-to 2016-01-29 12:07:27 +03:00
nvbn
abe8fb84c6 Merge branch 'master' of github.com:nvbn/thefuck 2016-01-29 12:04:07 +03:00
nvbn
86b17eb570 Fix travis builds 2016-01-29 12:03:47 +03:00
Pablo Santiago Blum de Aguiar
dcc0ce7ff3 #449 Fix Fish.how_to_configure()
Fix #449
2016-01-28 20:47:17 -02:00
Vladimir Iakovlev
fee874cddc Merge pull request #443 from aureooms/patch-1
new pattern in sudo rule
2016-01-24 16:45:17 +03:00
Aurélien Ooms
2ce1c6bf90 new pattern in sudo rule
`pacman-key --refresh-keys` outputs the following when not run as root

```
==> ERROR: pacman-key needs to be run as root for this operation.
```
2016-01-24 14:38:13 +01:00
nvbn
9b129fad08 #N/A: Add docsting in shells 2016-01-24 04:39:27 +03:00
nvbn
20adefbe7d Merge branch 'master' into simplify-shells 2016-01-23 23:49:26 +03:00
nvbn
52ef1fa47d #438: Replace [[ with [ in install script 2016-01-23 23:48:51 +03:00
nvbn
94f8652175 #N/A: Add tests for tcsh 2016-01-23 05:06:33 +03:00
nvbn
abe287a52b #N/A: Split shells module 2016-01-23 05:06:22 +03:00
nvbn
60e19a054a #N/A: Replace PY3 checks with PY2 checks 2016-01-22 17:02:08 +03:00
Vladimir Iakovlev
45add1e3d6 Merge pull request #437 from BuBuaBu/master
Exclude recursively tests packages
2016-01-22 16:56:29 +03:00
Vladimir Iakovlev
934870f650 Merge pull request #436 from scorphus/shelve-open-errors
Fix cache problem when going from Python 3 to 2
2016-01-22 16:56:08 +03:00
BuBuaBu
738cc0c9a8 Exclude recursively tests packages 2016-01-22 13:47:31 +01:00
Pablo Santiago Blum de Aguiar
cb6f964a30 Fix cache problem when going from Python 3 to 2 2016-01-21 23:57:17 -02:00
Vladimir Iakovlev
9c9a7343de Merge pull request #435 from scorphus/cache-fish-aliases
#353 Cache Fish aliases too
2016-01-21 18:40:11 +03:00
Pablo Santiago Blum de Aguiar
3803452980 #353 Cache Fish aliases too 2016-01-20 21:43:00 -02:00
Vladimir Iakovlev
a19833d0c7 Merge pull request #434 from scorphus/433-ioencoding
#433: Set env vars right in the aliases
2016-01-17 14:00:23 +03:00
Pablo Santiago Blum de Aguiar
084b907ac0 #433: Set env vars right in the aliases
Fix #433
2016-01-16 21:35:15 -02:00
94 changed files with 1670 additions and 901 deletions

View File

@@ -17,8 +17,9 @@ addons:
- pandoc
- git
install:
- pip install coveralls
- pip install -r requirements.txt
- pip install -U pip
- pip install -U coveralls
- pip install -Ur requirements.txt
- python setup.py develop
- rm -rf build
script:

View File

@@ -1,10 +1,10 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg?branch=master)](https://travis-ci.org/nvbn/thefuck)
# The Fuck [![Version][version-badge]][version-link] [![Build Status][travis-badge]][travis-link] [![Windows Build Status][appveyor-badge]][appveyor-link] [![Coverage][coverage-badge]][coverage-link] [![MIT License][license-badge]](LICENSE.md)
Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320).
[![gif with examples](https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif)](https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif)
[![gif with examples][examples-link]][examples-link]
Few more examples:
@@ -52,7 +52,7 @@ Python 3.4.2 (default, Oct 8 2014, 13:08:17)
git: 'brnch' is not a git command. See 'git --help'.
Did you mean this?
branch
branch
➜ fuck
git branch [enter/↑/↓/ctrl+c]
@@ -97,7 +97,7 @@ Reading package lists... Done
## Installation [*experimental*]
On Ubuntu and OS X you can install `The Fuck` with installation script:
```bash
wget -O - https://raw.githubusercontent.com/nvbn/thefuck/master/install.sh | sh - && $0
```
@@ -107,7 +107,7 @@ wget -O - https://raw.githubusercontent.com/nvbn/thefuck/master/install.sh | sh
Install `The Fuck` with `pip`:
```bash
sudo pip install thefuck
sudo -H pip install thefuck
```
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
@@ -144,6 +144,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `cd_correction` – spellchecks and correct failed cd commands;
* `cd_mkdir` – creates directories before cd'ing into them;
* `cd_parent` – changes `cd..` to `cd ..`;
* `chmod_x` – add execution bit;
* `composer_not_command` – fixes composer command name;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`;
@@ -155,20 +156,25 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `dry` – fixes repetitions like `git git push`;
* `fix_alt_space` – replaces Alt+Space with Space character;
* `fix_file` – opens a file with an error in your `$EDITOR`;
* `git_add` – fixes *"Did you forget to 'git add'?"*;
* `git_add` – fixes *"pathspec 'foo' did not match any file(s) known to git."*;
* `git_branch_delete` – changes `git branch -d` to `git branch -D`;
* `git_branch_exists` – offers `git branch -d foo`, `git branch -D foo` or `git checkout foo` when creating a branch that already exists;
* `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` – fixes branch name or creates new branch;
* `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output;
* `git_fix_stash` – fixes `git stash` commands (misspelled subcommand and missing `save`);
* `git_help_aliased` &ndash; fixes `git help <alias>` commands replacing <alias> with the aliased command;
* `git_not_command` &ndash; fixes wrong git commands like `git brnch`;
* `git_pull` &ndash; sets upstream before executing previous `git pull`;
* `git_pull_clone` &ndash; clones instead of pulling when the repo does not exist;
* `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` &ndash; runs `git pull` when `push` was rejected;
* `git_rm_recursive` &ndash; adds `-r` when you try to `rm` a directory;
* `git_remote_seturl_add` &ndash; runs `git remote add` when `git remote set_url` on nonexistant remote;
* `git_stash` &ndash; stashes you local modifications before rebasing or switching branch;
* `git_two_dashes` &ndash; adds a missing dash to commands like `git commit -amend` or `git rebase -continue`;
* `go_run` &ndash; appends `.go` extension when compiling/running Go programs
* `go_run` &ndash; appends `.go` extension when compiling/running Go programs;
* `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;
* `gulp_not_task` &ndash; fixes misspelled `gulp` tasks;
* `has_exists_script` &ndash; prepends `./` when script/binary exists;
@@ -177,6 +183,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `java` &ndash; removes `.java` extension when running Java programs;
* `javac` &ndash; appends missing `.java` when compiling Java files;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `ln_no_hard_link` &ndash; catches hard link creation on directories, suggest symbolic link;
* `ls_lah` &ndash; adds `-lah` to `ls`;
* `man` &ndash; changes manual section;
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
@@ -184,6 +191,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent;
* `mvn_no_command` &ndash; adds `clean package` to `mvn`;
* `mvn_unknown_lifecycle_phase` &ndash; fixes misspelled lifecycle phases with `mvn`;
* `npm_wrong_command` &ndash; fixes wrong npm commands like `npm urgrade`;
* `no_command` &ndash; fixes wrong console commands, for example `vom/vim`;
* `no_such_file` &ndash; creates missing directories with `mv` and `cp` commands;
* `open` &ndash; prepends `http` to address passed to `open`;
@@ -214,6 +222,7 @@ Enabled by default only on specific platforms:
* `apt_invalid_operation` &ndash; fixes invalid `apt` and `apt-get` calls, like `apt-get isntall vim`;
* `brew_install` &ndash; fixes formula name for `brew install`;
* `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_upgrade` &ndash; appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yaourt` if available);
* `pacman_not_found` &ndash; fixes package name with `pacman` or `yaourt`.
@@ -300,7 +309,7 @@ debug = False
Or via environment variables:
* `THEFUCK_RULES` &ndash; list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`;
* `THEFUCK_EXCLUDE_RULES` &ndash; list of disabled rules, like `git_pull:git_push`;
* `THEFUCK_EXCLUDE_RULES` &ndash; list of disabled rules, like `git_pull:git_push`;
* `THEFUCK_REQUIRE_CONFIRMATION` &ndash; require confirmation before running new command, `true/false`;
* `THEFUCK_WAIT_COMMAND` &ndash; max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` &ndash; disable colored output, `true/false`;
@@ -352,3 +361,15 @@ sudo apt-get install pandoc
## License MIT
Project License can be found [here](LICENSE.md).
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
[version-link]: https://pypi.python.org/pypi/thefuck/
[travis-badge]: https://img.shields.io/travis/nvbn/thefuck.svg
[travis-link]: https://travis-ci.org/nvbn/thefuck
[appveyor-badge]: https://img.shields.io/appveyor/ci/nvbn/thefuck.svg?label=windows%20build
[appveyor-link]: https://ci.appveyor.com/project/nvbn/thefuck
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
[coverage-link]: https://coveralls.io/github/nvbn/thefuck
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
[examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif

22
appveyor.yml Normal file
View File

@@ -0,0 +1,22 @@
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
- PYTHON: "C:/Python33"
- PYTHON: "C:/Python34"
- PYTHON: "C:/Python35"
init:
- "ECHO %PYTHON%"
- ps: "ls C:/Python*"
install:
- ps: (new-object net.webclient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', 'C:/get-pip.py')
- "%PYTHON%/python.exe C:/get-pip.py"
- "%PYTHON%/Scripts/pip.exe install -U setuptools"
- "%PYTHON%/python.exe setup.py develop"
- "%PYTHON%/Scripts/pip.exe install -U -r requirements.txt"
test_script:
- "%PYTHON%/Scripts/py.test.exe -sv"

View File

@@ -15,7 +15,7 @@ install_thefuck () {
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found python-gdbm
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
if [ -n "$(apt-cache search python-commandnotfound)" ]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi

View File

@@ -1,3 +1,4 @@
pip
pytest
mock
pytest-mock

View File

@@ -1,8 +1,14 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import pip
import sys
import os
if int(pip.__version__.split('.')[0]) < 6:
print('pip older than 6.0 not supported, please upgrade pip with:\n\n'
' pip install -U pip')
sys.exit(-1)
if os.environ.get('CONVERT_README'):
import pypandoc
@@ -20,7 +26,7 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.3'
VERSION = '3.9'
install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib'],
@@ -35,7 +41,7 @@ setup(name='thefuck',
url='https://github.com/nvbn/thefuck',
license='MIT',
packages=find_packages(exclude=['ez_setup', 'examples',
'tests', 'release']),
'tests', 'tests.*', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=install_requires,

View File

@@ -1,6 +1,9 @@
from pathlib import Path
import pytest
from thefuck import conf
from thefuck import shells
from thefuck import conf, const
shells.shell = shells.Generic()
def pytest_addoption(parser):
@@ -19,7 +22,7 @@ def no_memoize(monkeypatch):
def settings(request):
def _reset_settings():
conf.settings.clear()
conf.settings.update(conf.DEFAULT_SETTINGS)
conf.settings.update(const.DEFAULT_SETTINGS)
request.addfinalizer(_reset_settings)
conf.settings.user_dir = Path('~/.thefuck')
@@ -46,3 +49,14 @@ def functional(request):
@pytest.fixture
def source_root():
return Path(__file__).parent.parent.resolve()
@pytest.fixture
def set_shell(monkeypatch, request):
def _set(cls):
shell = cls()
monkeypatch.setattr('thefuck.shells.shell', shell)
request.addfinalizer()
return shell
return _set

View File

@@ -49,7 +49,7 @@ def test_select_command_with_arrows(proc, TIMEOUT):
@pytest.mark.functional
@pytest.mark.once_without_docker
@pytest.mark.skip_without_docker
def test_refuse_with_confirmation(proc, TIMEOUT):
refuse_with_confirmation(proc, TIMEOUT)
history_not_changed(proc, TIMEOUT)

View File

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

View File

@@ -29,12 +29,27 @@ def test_match_mocked(cmdnf_mock, command, return_value):
assert get_packages.called
# python-commandnotfound is available in ubuntu 14.04+
@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True),
reason='Skip if python-commandnotfound is not available')
@pytest.mark.parametrize('command', [
Command(script='a_bad_cmd', stderr='a_bad_cmd: command not found'),
Command(script='vim', stderr=''), Command()])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, return_value', [
(Command(script='a_bad_cmd', stderr='a_bad_cmd: command not found'), []),
(Command(script='vim', stderr=''), []), (Command(), [])])
@patch('thefuck.rules.apt_get.CommandNotFound', create=True)
@patch.multiple(apt_get, create=True, apt_get='apt_get')
def test_not_match_mocked(cmdnf_mock, command, return_value):
get_packages = Mock(return_value=return_value)
cmdnf_mock.CommandNotFound.return_value = Mock(getPackages=get_packages)
assert not match(command)
# python-commandnotfound is available in ubuntu 14.04+
@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True),
reason='Skip if python-commandnotfound is not available')

View File

@@ -0,0 +1,30 @@
import pytest
from tests.utils import Command
from thefuck.rules.brew_update_formula import get_new_command, match
@pytest.fixture
def stderr():
return ("Error: This command updates brew itself, and does not take formula"
" names.\nUse 'brew upgrade <formula>'.")
@pytest.fixture
def new_command(formula):
return 'brew upgrade {}'.format(formula)
@pytest.mark.parametrize('script', ['brew update foo', 'brew update bar zap'])
def test_match(stderr, script):
assert match(Command(script=script, stderr=stderr))
@pytest.mark.parametrize('script', ['brew upgrade foo', 'brew update'])
def test_not_match(script):
assert not match(Command(script=script, stderr=''))
@pytest.mark.parametrize('script, formula, ', [
('brew update foo', 'foo'), ('brew update bar zap', 'bar zap')])
def test_get_new_command(stderr, new_command, script, formula):
assert get_new_command(Command(script=script, stderr=stderr)) == new_command

View File

@@ -0,0 +1,39 @@
import pytest
from tests.utils import Command
from thefuck.rules.chmod_x import match, get_new_command
@pytest.fixture
def file_exists(mocker):
return mocker.patch('os.path.exists', return_value=True)
@pytest.fixture
def file_access(mocker):
return mocker.patch('os.access', return_value=False)
@pytest.mark.usefixtures('file_exists', 'file_access')
@pytest.mark.parametrize('script, stderr', [
('./gradlew build', 'gradlew: Permission denied'),
('./install.sh --help', 'install.sh: permission denied')])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr, exists, callable', [
('./gradlew build', 'gradlew: Permission denied', True, True),
('./gradlew build', 'gradlew: Permission denied', False, False),
('./gradlew build', 'gradlew: error', True, False),
('gradlew build', 'gradlew: Permission denied', True, False)])
def test_not_match(file_exists, file_access, script, stderr, exists, callable):
file_exists.return_value = exists
file_access.return_value = callable
assert not match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, result', [
('./gradlew build', 'chmod +x gradlew && ./gradlew build'),
('./install.sh --help', 'chmod +x install.sh && ./install.sh --help')])
def test_get_new_command(script, result):
assert get_new_command(Command(script)) == result

View File

@@ -4,36 +4,28 @@ from tests.utils import Command
@pytest.fixture
def did_not_match(target, did_you_forget=True):
error = ("error: pathspec '{}' did not match any "
"file(s) known to git.".format(target))
if did_you_forget:
error = ("{}\nDid you forget to 'git add'?'".format(error))
return error
def stderr(target):
return ("error: pathspec '{}' did not match any "
'file(s) known to git.'.format(target))
@pytest.mark.parametrize('command', [
Command(script='git submodule update unknown',
stderr=did_not_match('unknown')),
Command(script='git commit unknown',
stderr=did_not_match('unknown'))]) # Older versions of Git
def test_match(command):
assert match(command)
@pytest.mark.parametrize('script, target', [
('git submodule update unknown', 'unknown'),
('git commit unknown', 'unknown')])
def test_match(stderr, script, target):
assert match(Command(script=script, stderr=stderr))
@pytest.mark.parametrize('command', [
Command(script='git submodule update known', stderr=('')),
Command(script='git commit known', stderr=('')),
Command(script='git commit unknown', # Newer versions of Git
stderr=did_not_match('unknown', False))])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('script', [
'git submodule update known', 'git commit known'])
def test_not_match(script):
assert not match(Command(script=script, stderr=''))
@pytest.mark.parametrize('command, new_command', [
(Command('git submodule update unknown', stderr=did_not_match('unknown')),
@pytest.mark.parametrize('script, target, new_command', [
('git submodule update unknown', 'unknown',
'git add -- unknown && git submodule update unknown'),
(Command('git commit unknown', stderr=did_not_match('unknown')), # Old Git
('git commit unknown', 'unknown',
'git add -- unknown && git commit unknown')])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command
def test_get_new_command(stderr, script, target, new_command):
assert get_new_command(Command(script=script, stderr=stderr)) == new_command

View File

@@ -0,0 +1,33 @@
import pytest
from thefuck.rules.git_branch_exists import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr(branch_name):
return "fatal: A branch named '{}' already exists.".format(branch_name)
@pytest.fixture
def new_command(branch_name):
return [cmd.format(branch_name) for cmd in [
'git branch -d {0} && git branch {0}',
'git branch -D {0} && git branch {0}', 'git checkout {0}']]
@pytest.mark.parametrize('script, branch_name', [
('git branch foo', 'foo'),
('git branch bar', 'bar')])
def test_match(stderr, script, branch_name):
assert match(Command(script=script, stderr=stderr))
@pytest.mark.parametrize('script', ['git branch foo', 'git branch bar'])
def test_not_match(script):
assert not match(Command(script=script, stderr=''))
@pytest.mark.parametrize('script, branch_name, ', [
('git branch foo', 'foo'), ('git branch bar', 'bar')])
def test_get_new_command(stderr, new_command, script, branch_name):
assert get_new_command(Command(script=script, stderr=stderr)) == new_command

View File

@@ -1,5 +1,5 @@
from thefuck import shells
from thefuck.rules.git_branch_list import match, get_new_command
from thefuck.shells import shell
from tests.utils import Command
@@ -16,4 +16,4 @@ def test_not_match():
def test_get_new_command():
assert (get_new_command(Command('git branch list')) ==
shells.and_('git branch --delete list', 'git branch'))
shell.and_('git branch --delete list', 'git branch'))

View File

@@ -1,5 +1,6 @@
import pytest
from thefuck.rules.git_checkout import match, get_new_command
from io import BytesIO
from thefuck.rules.git_checkout import match, get_branches, get_new_command
from tests.utils import Command
@@ -13,8 +14,10 @@ def did_not_match(target, did_you_forget=False):
@pytest.fixture
def get_branches(mocker):
return mocker.patch('thefuck.rules.git_checkout.get_branches')
def git_branch(mocker, branches):
mock = mocker.patch('subprocess.Popen')
mock.return_value.stdout = BytesIO(branches)
return mock
@pytest.mark.parametrize('command', [
@@ -33,21 +36,34 @@ def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('branches, branch_list', [
(b'', []),
(b'* master', ['master']),
(b' remotes/origin/master', ['master']),
(b' just-another-branch', ['just-another-branch']),
(b'* master\n just-another-branch', ['master', 'just-another-branch']),
(b'* master\n remotes/origin/master\n just-another-branch',
['master', 'master', 'just-another-branch'])])
def test_get_branches(branches, branch_list, git_branch):
git_branch(branches)
assert list(get_branches()) == branch_list
@pytest.mark.parametrize('branches, command, new_command', [
([],
(b'',
Command(script='git checkout unknown', stderr=did_not_match('unknown')),
'git branch unknown && git checkout unknown'),
([],
(b'',
Command('git commit unknown', stderr=did_not_match('unknown')),
'git branch unknown && git commit unknown'),
(['test-random-branch-123'],
(b' test-random-branch-123',
Command(script='git checkout tst-rdm-brnch-123',
stderr=did_not_match('tst-rdm-brnch-123')),
'git checkout test-random-branch-123'),
(['test-random-branch-123'],
(b' test-random-branch-123',
Command(script='git commit tst-rdm-brnch-123',
stderr=did_not_match('tst-rdm-brnch-123')),
'git commit test-random-branch-123')])
def test_get_new_command(branches, command, new_command, get_branches):
get_branches.return_value = branches
def test_get_new_command(branches, command, new_command, git_branch):
git_branch(branches)
assert get_new_command(command) == new_command

View File

@@ -0,0 +1,24 @@
import pytest
from thefuck.rules.git_help_aliased import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('script, stdout', [
('git help st', "`git st' is aliased to `status'"),
('git help ds', "`git ds' is aliased to `diff --staged'")])
def test_match(script, stdout):
assert match(Command(script=script, stdout=stdout))
@pytest.mark.parametrize('script, stdout', [
('git help status', "GIT-STATUS(1)...Git Manual...GIT-STATUS(1)"),
('git help diff', "GIT-DIFF(1)...Git Manual...GIT-DIFF(1)")])
def test_not_match(script, stdout):
assert not match(Command(script=script, stdout=stdout))
@pytest.mark.parametrize('script, stdout, new_command', [
('git help st', "`git st' is aliased to `status'", 'git help status'),
('git help ds', "`git ds' is aliased to `diff --staged'", 'git help diff')])
def test_get_new_command(script, stdout, new_command):
assert get_new_command(Command(script=script, stdout=stdout)) == new_command

View File

@@ -0,0 +1,26 @@
import pytest
from thefuck.rules.git_remote_seturl_add import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='git remote set-url origin url', stderr="fatal: No such remote")])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('git remote set-url origin url', stderr=""),
Command('git remote add origin url'),
Command('git remote remove origin'),
Command('git remote prune origin'),
Command('git remote set-branches origin branch')
])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('git remote set-url origin git@github.com:nvbn/thefuck.git'),
'git remote add origin git@github.com:nvbn/thefuck.git')])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -0,0 +1,27 @@
import pytest
from thefuck.rules.git_rm_recursive import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr(target):
return "fatal: not removing '{}' recursively without -r".format(target)
@pytest.mark.parametrize('script, target', [
('git rm foo', 'foo'),
('git rm foo bar', 'foo bar')])
def test_match(stderr, script, target):
assert match(Command(script=script, stderr=stderr))
@pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar'])
def test_not_match(script):
assert not match(Command(script=script, stderr=''))
@pytest.mark.parametrize('script, target, new_command', [
('git rm foo', 'foo', 'git rm -r foo'),
('git rm foo bar', 'foo bar', 'git rm -r foo bar')])
def test_get_new_command(stderr, script, target, new_command):
assert get_new_command(Command(script=script, stderr=stderr)) == new_command

View File

@@ -0,0 +1,40 @@
import pytest
from thefuck.rules.grep_arguments_order import get_new_command, match
from tests.utils import Command
stderr = 'grep: {}: No such file or directory'.format
@pytest.fixture(autouse=True)
def os_path(monkeypatch):
monkeypatch.setattr('os.path.isfile', lambda x: not x.startswith('-'))
@pytest.mark.parametrize('script, file', [
('grep test.py test', 'test'),
('grep -lir . test', 'test'),
('egrep test.py test', 'test'),
('egrep -lir . test', 'test')])
def test_match(script, file):
assert match(Command(script, stderr=stderr(file)))
@pytest.mark.parametrize('script, stderr', [
('cat test.py', stderr('test')),
('grep test test.py', ''),
('grep -lir test .', ''),
('egrep test test.py', ''),
('egrep -lir test .', '')])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr, result', [
('grep test.py test', stderr('test'), 'grep test test.py'),
('grep -lir . test', stderr('test'), 'grep -lir test .'),
('grep . test -lir', stderr('test'), 'grep test -lir .'),
('egrep test.py test', stderr('test'), 'egrep test test.py'),
('egrep -lir . test', stderr('test'), 'egrep -lir test .'),
('egrep . test -lir', stderr('test'), 'egrep test -lir .')])
def test_get_new_command(script, stderr, result):
assert get_new_command(Command(script, stderr=stderr)) == result

View File

@@ -1,4 +1,5 @@
import pytest
from io import BytesIO
from tests.utils import Command
from thefuck.rules.gulp_not_task import match, get_new_command
@@ -22,7 +23,7 @@ def test_not_march(script, stdout):
def test_get_new_command(mocker):
mocker.patch('thefuck.rules.gulp_not_task.get_gulp_tasks', return_value=[
'serve', 'build', 'default'])
mock = mocker.patch('subprocess.Popen')
mock.return_value.stdout = BytesIO(b'serve \nbuild \ndefault \n')
command = Command('gulp srve', stdout('srve'))
assert get_new_command(command) == ['gulp serve', 'gulp default']

View File

@@ -3,38 +3,23 @@ from thefuck.rules.history import match, get_new_command
from tests.utils import Command
@pytest.fixture
def history(mocker):
return mocker.patch('thefuck.rules.history.get_history',
return_value=['le cat', 'fuck', 'ls cat',
'diff x', 'nocommand x'])
@pytest.fixture(autouse=True)
def history_without_current(mocker):
return mocker.patch(
'thefuck.rules.history.get_valid_history_without_current',
return_value=['ls cat', 'diff x'])
@pytest.fixture
def alias(mocker):
return mocker.patch('thefuck.rules.history.thefuck_alias',
return_value='fuck')
@pytest.fixture
def callables(mocker):
return mocker.patch('thefuck.rules.history.get_all_executables',
return_value=['diff', 'ls'])
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script', ['ls cet', 'daff x'])
def test_match(script):
assert match(Command(script=script))
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script', ['apt-get', 'nocommand y'])
def test_not_match(script):
assert not match(Command(script=script))
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script, result', [
('ls cet', 'ls cat'),
('daff x', 'diff x')])

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.rules.ln_no_hard_link import match, get_new_command
from tests.utils import Command
error = "hard link not allowed for directory"
@pytest.mark.parametrize('script, stderr', [
("ln barDir barLink", "ln: barDir: {}"),
("sudo ln a b", "ln: a: {}"),
("sudo ln -nbi a b", "ln: a: {}")])
def test_match(script, stderr):
command = Command(script, stderr=stderr.format(error))
assert match(command)
@pytest.mark.parametrize('script, stderr', [
('', ''),
("ln a b", "... hard link"),
("sudo ln a b", "... hard link"),
("a b", error)])
def test_assert_not_match(script, stderr):
command = Command(script, stderr=stderr)
assert not match(command)
@pytest.mark.parametrize('script, result', [
("ln barDir barLink", "ln -s barDir barLink"),
("sudo ln barDir barLink", "sudo ln -s barDir barLink"),
("sudo ln -nbi a b", "sudo ln -s -nbi a b"),
("ln -nbi a b && ls", "ln -s -nbi a b && ls"),
("ln a ln", "ln -s a ln"),
("sudo ln a ln", "sudo ln -s a ln")])
def test_get_new_command(script, result):
command = Command(script)
assert get_new_command(command) == result

View File

@@ -6,22 +6,37 @@ from tests.utils import Command
@pytest.fixture(autouse=True)
def get_all_executables(mocker):
mocker.patch('thefuck.rules.no_command.get_all_executables',
return_value=['vim', 'apt-get', 'fsck'])
return_value=['vim', 'fsck', 'git', 'go'])
@pytest.fixture(autouse=True)
def history_without_current(mocker):
return mocker.patch(
'thefuck.rules.no_command.get_valid_history_without_current',
return_value=['git commit'])
@pytest.mark.usefixtures('no_memoize')
def test_match():
assert match(Command(stderr='vom: not found', script='vom file.py'))
assert match(Command(stderr='fucck: not found', script='fucck'))
assert not match(Command(stderr='qweqwe: not found', script='qweqwe'))
assert not match(Command(stderr='some text', script='vom file.py'))
@pytest.mark.parametrize('script, stderr', [
('vom file.py', 'vom: not found'),
('fucck', 'fucck: not found'),
('got commit', 'got: command not found')])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr))
@pytest.mark.usefixtures('no_memoize')
def test_get_new_command():
assert get_new_command(
Command(stderr='vom: not found',
script='vom file.py')) == ['vim file.py']
assert get_new_command(
Command(stderr='fucck: not found',
script='fucck')) == ['fsck']
@pytest.mark.parametrize('script, stderr', [
('qweqwe', 'qweqwe: not found'),
('vom file.py', 'some text')])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr))
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, result', [
('vom file.py', ['vim file.py']),
('fucck', ['fsck']),
('got commit', ['git commit', 'go commit'])])
def test_get_new_command(script, result):
assert get_new_command(Command(script)) == result

View File

@@ -0,0 +1,58 @@
import pytest
from thefuck.rules.npm_wrong_command import match, get_new_command
from tests.utils import Command
stdout = '''
Usage: npm <command>
where <command> is one of:
access, add-user, adduser, apihelp, author, bin, bugs, c,
cache, completion, config, ddp, dedupe, deprecate, dist-tag,
dist-tags, docs, edit, explore, faq, find, find-dupes, get,
help, help-search, home, i, info, init, install, issues, la,
link, list, ll, ln, login, logout, ls, outdated, owner,
pack, ping, prefix, prune, publish, r, rb, rebuild, remove,
repo, restart, rm, root, run-script, s, se, search, set,
show, shrinkwrap, star, stars, start, stop, t, tag, team,
test, tst, un, uninstall, unlink, unpublish, unstar, up,
update, upgrade, v, verison, version, view, whoami
npm <cmd> -h quick help on <cmd>
npm -l display full usage info
npm faq commonly asked questions
npm help <term> search for help on <term>
npm help npm involved overview
Specify configs in the ini-formatted file:
/home/nvbn/.npmrc
or on the command line via: npm <command> --key value
Config info can be viewed via: npm help config
npm@2.14.7 /opt/node/lib/node_modules/npm
'''
@pytest.mark.parametrize('script', [
'npm urgrdae',
'npm urgrade -g',
'npm -f urgrade -g',
'npm urg'])
def test_match(script):
assert match(Command(script, stdout))
@pytest.mark.parametrize('script, stdout', [
('npm urgrade', ''),
('npm', stdout),
('test urgrade', stdout),
('npm -e', stdout)])
def test_not_match(script, stdout):
assert not match(Command(script, stdout))
@pytest.mark.parametrize('script, result', [
('npm urgrade', 'npm upgrade'),
('npm -g isntall gulp', 'npm -g install gulp'),
('npm isntall -g gulp', 'npm install -g gulp')])
def test_get_new_command(script, result):
assert get_new_command(Command(script, stdout)) == result

View File

@@ -52,6 +52,7 @@ def test_match(ssh_error):
assert not match(Command('ssh'))
@pytest.mark.skipif(os.name == 'nt', reason='Skip if testing on Windows')
def test_side_effect(ssh_error):
errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', stderr=errormsg)

0
tests/shells/__init__.py Normal file
View File

22
tests/shells/conftest.py Normal file
View File

@@ -0,0 +1,22 @@
import pytest
@pytest.fixture
def builtins_open(mocker):
return mocker.patch('six.moves.builtins.open')
@pytest.fixture
def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
@pytest.fixture
@pytest.mark.usefixtures('isfile')
def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__ \
.return_value.readlines.return_value = lines
return aux

58
tests/shells/test_bash.py Normal file
View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
import os
import pytest
from thefuck.shells import Bash
@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestBash(object):
@pytest.fixture
def shell(self):
return Bash()
@pytest.fixture(autouse=True)
def shell_aliases(self):
os.environ['TF_SHELL_ALIASES'] = (
'alias fuck=\'eval $(thefuck $(fc -ln -1))\'\n'
'alias l=\'ls -CF\'\n'
'alias la=\'ls -A\'\n'
'alias ll=\'ls -alF\'')
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('fuck', 'eval $(thefuck $(fc -ln -1))'),
('awk', 'awk'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS=fuck' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_app_alias_variables_correctly_set(self, shell):
alias = shell.app_alias('fuck')
assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias
assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias
assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias
assert 'ALIASES=$(alias) thefuck' in alias
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm']

89
tests/shells/test_fish.py Normal file
View File

@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.shells import Fish
@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestFish(object):
@pytest.fixture
def shell(self):
return Fish()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.fish.Popen')
mock.return_value.stdout.read.return_value = (
b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby')
return mock
@pytest.fixture
def os_environ(self, monkeypatch, key, value):
monkeypatch.setattr('os.environ', {key: value})
@pytest.mark.parametrize('key, value', [
('TF_OVERRIDDEN_ALIASES', 'cut,git,sed'), # legacy
('THEFUCK_OVERRIDDEN_ALIASES', 'cut,git,sed'),
('THEFUCK_OVERRIDDEN_ALIASES', 'cut, git, sed'),
('THEFUCK_OVERRIDDEN_ALIASES', ' cut,\tgit,sed\n'),
('THEFUCK_OVERRIDDEN_ALIASES', '\ncut,\n\ngit,\tsed\r')])
def test_get_overridden_aliases(self, shell, os_environ):
assert shell._get_overridden_aliases() == {'cd', 'cut', 'git', 'grep',
'ls', 'man', 'open', 'sed'}
@pytest.mark.parametrize('before, after', [
('cd', 'cd'),
('pwd', 'pwd'),
('fuck', 'fish -ic "fuck"'),
('find', 'find'),
('funced', 'fish -ic "funced"'),
('grep', 'grep'),
('awk', 'awk'),
('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
('man', 'man'),
('open', 'open'),
('vim', 'vim'),
('ll', 'fish -ic "ll"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fish_config': 'fish_config',
'fuck': 'fuck',
'funced': 'funced',
'funcsave': 'funcsave',
'history': 'history',
'll': 'll',
'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'ruby': 'ruby'}
def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias('fuck')
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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')
def test_get_history(self, history_lines, shell):
history_lines(['- cmd: ls', ' when: 1432613911',
'- cmd: rm', ' when: 1432613916'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', '- cmd: ls\n when: 1430707243\n'),
(u'echo café', '- cmd: echo café\n when: 1430707243\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.fish.time', return_value=1430707243.3517463)
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.shells import Generic
class TestGeneric(object):
@pytest.fixture
def shell(self):
return Generic()
def test_from_shell(self, shell):
assert shell.from_shell('pwd') == 'pwd'
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
# We don't know what to do in generic shell with history lines,
# so just ignore them:
assert list(shell.get_history()) == []
def test_split_command(self, shell):
assert shell.split_command('ls') == ['ls']
assert shell.split_command(u'echo café') == [u'echo', u'café']

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.shells import Powershell
@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestPowershell(object):
@pytest.fixture
def shell(self):
return Powershell()
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == '(ls) -and (cd)'
def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias('fuck')
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')

50
tests/shells/test_tcsh.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.shells.tcsh import Tcsh
@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestTcsh(object):
@pytest.fixture
def shell(self):
return Tcsh()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.tcsh.Popen')
mock.return_value.stdout.read.return_value = (
b'fuck\teval $(thefuck $(fc -ln -1))\n'
b'l\tls -CF\n'
b'la\tls -A\n'
b'll\tls -alF')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('fuck', 'eval $(thefuck $(fc -ln -1))'),
('awk', 'awk'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm']

57
tests/shells/test_zsh.py Normal file
View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import os
import pytest
from thefuck.shells.zsh import Zsh
@pytest.mark.usefixtures('isfile', 'no_memoize', 'no_cache')
class TestZsh(object):
@pytest.fixture
def shell(self):
return Zsh()
@pytest.fixture(autouse=True)
def shell_aliases(self):
os.environ['TF_SHELL_ALIASES'] = (
'fuck=\'eval $(thefuck $(fc -ln -1 | tail -n 1))\'\n'
'l=\'ls -CF\'\n'
'la=\'ls -A\'\n'
'll=\'ls -alF\'')
@pytest.mark.parametrize('before, after', [
('fuck', 'eval $(thefuck $(fc -ln -1 | tail -n 1))'),
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {
'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING' in shell.app_alias('fuck')
def test_app_alias_variables_correctly_set(self, shell):
alias = shell.app_alias('fuck')
assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias
assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias
assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias
assert 'ALIASES=$(alias) thefuck' in alias
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])
assert list(shell.get_history()) == ['ls', 'rm']

View File

@@ -1,7 +1,7 @@
import pytest
import six
from mock import Mock
from thefuck import conf
from thefuck import const
@pytest.fixture
@@ -20,7 +20,7 @@ def environ(monkeypatch):
def test_settings_defaults(load_source, settings):
load_source.return_value = object()
settings.init()
for key, val in conf.DEFAULT_SETTINGS.items():
for key, val in const.DEFAULT_SETTINGS.items():
assert getattr(settings, key) == val
@@ -42,13 +42,13 @@ class TestSettingsFromFile(object):
assert settings.exclude_rules == ['git']
def test_from_file_with_DEFAULT(self, load_source, settings):
load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'],
load_source.return_value = Mock(rules=const.DEFAULT_RULES + ['test'],
wait_command=10,
exclude_rules=[],
require_confirmation=True,
no_colors=True)
settings.init()
assert settings.rules == conf.DEFAULT_RULES + ['test']
assert settings.rules == const.DEFAULT_RULES + ['test']
@pytest.mark.usefixture('load_source')
@@ -71,7 +71,7 @@ class TestSettingsFromEnv(object):
def test_from_env_with_DEFAULT(self, environ, settings):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
settings.init()
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']
assert settings.rules == const.DEFAULT_RULES + ['bash', 'lisp']
class TestInitializeSettingsFile(object):
@@ -93,7 +93,7 @@ class TestInitializeSettingsFile(object):
settings_file_contents = settings_file.getvalue()
assert settings_path_mock.is_file.call_count == 1
assert settings_path_mock.open.call_count == 1
assert conf.SETTINGS_HEADER in settings_file_contents
for setting in conf.DEFAULT_SETTINGS.items():
assert const.SETTINGS_HEADER in settings_file_contents
for setting in const.DEFAULT_SETTINGS.items():
assert '# {} = {}\n'.format(*setting) in settings_file_contents
settings_file.close()

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
import pytest
from pathlib import PosixPath
from thefuck import corrector, conf
from pathlib import Path
from thefuck import corrector, const
from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import get_corrected_commands, organize_commands
@@ -24,13 +24,13 @@ class TestGetRules(object):
assert {r.name for r in rules} == set(names)
@pytest.mark.parametrize('paths, conf_rules, exclude_rules, loaded_rules', [
(['git.py', 'bash.py'], conf.DEFAULT_RULES, [], ['git', 'bash']),
(['git.py', 'bash.py'], const.DEFAULT_RULES, [], ['git', 'bash']),
(['git.py', 'bash.py'], ['git'], [], ['git']),
(['git.py', 'bash.py'], conf.DEFAULT_RULES, ['git'], ['bash']),
(['git.py', 'bash.py'], const.DEFAULT_RULES, ['git'], ['bash']),
(['git.py', 'bash.py'], ['git'], ['git'], [])])
def test_get_rules(self, glob, settings, paths, conf_rules, exclude_rules,
loaded_rules):
glob([PosixPath(path) for path in paths])
glob([Path(path) for path in paths])
settings.update(rules=conf_rules,
priority={},
exclude_rules=exclude_rules)

View File

@@ -1,260 +0,0 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck import shells
@pytest.fixture
def builtins_open(mocker):
return mocker.patch('six.moves.builtins.open')
@pytest.fixture
def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
@pytest.fixture
@pytest.mark.usefixtures('isfile')
def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.readlines.return_value = lines
return aux
class TestGeneric(object):
@pytest.fixture
def shell(self):
return shells.Generic()
def test_from_shell(self, shell):
assert shell.from_shell('pwd') == 'pwd'
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, shell):
assert shell.put_to_history('ls') is None
assert shell.put_to_history(u'echo café') is None
assert builtins_open.call_count == 0
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
# We don't know what to do in generic shell with history lines,
# so just ignore them:
assert list(shell.get_history()) == []
def test_split_command(self, shell):
assert shell.split_command('ls') == ['ls']
assert shell.split_command(u'echo café') == [u'echo', u'café']
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@pytest.fixture
def shell(self):
return shells.Bash()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'alias fuck=\'eval $(thefuck $(fc -ln -1))\'\n'
b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n'
b'alias ll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('fuck', 'eval $(thefuck $(fc -ln -1))'),
('awk', 'awk'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', 'ls\n'),
(u'echo café', 'echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, shell):
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile')
class TestFish(object):
@pytest.fixture
def shell(self):
return shells.Fish()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby')
return mock
@pytest.fixture
def environ(self, monkeypatch):
data = {'TF_OVERRIDDEN_ALIASES': 'cd, ls, man, open'}
monkeypatch.setattr('thefuck.shells.os.environ', data)
return data
@pytest.mark.usefixture('environ')
def test_get_overridden_aliases(self, shell, environ):
assert shell._get_overridden_aliases() == ['cd', 'ls', 'man', 'open']
@pytest.mark.parametrize('before, after', [
('cd', 'cd'),
('pwd', 'pwd'),
('fuck', 'fish -ic "fuck"'),
('find', 'find'),
('funced', 'fish -ic "funced"'),
('grep', 'grep'),
('awk', 'awk'),
('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
('man', 'man'),
('open', 'open'),
('vim', 'vim'),
('ll', 'fish -ic "ll"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', '- cmd: ls\n when: 1430707243\n'),
(u'echo café', '- cmd: echo café\n when: 1430707243\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'fish_config': 'fish_config',
'fuck': 'fuck',
'funced': 'funced',
'funcsave': 'funcsave',
'history': 'history',
'll': 'll',
'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'ruby': 'ruby'}
def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias('fuck')
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['- cmd: ls', ' when: 1432613911',
'- cmd: rm', ' when: 1432613916'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile')
class TestZsh(object):
@pytest.fixture
def shell(self):
return shells.Zsh()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'fuck=\'eval $(thefuck $(fc -ln -1 | tail -n 1))\'\n'
b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n'
b'll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('fuck', 'eval $(thefuck $(fc -ln -1 | tail -n 1))'),
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@pytest.mark.parametrize('entry, entry_utf8', [
('ls', ': 1430707243:0;ls\n'),
(u'echo café', ': 1430707243:0;echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {
'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])
assert list(shell.get_history()) == ['ls', 'rm']

View File

@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
import os
from subprocess import PIPE
from mock import Mock
from pathlib import Path
import pytest
from tests.utils import CorrectedCommand, Rule, Command
from thefuck import conf
from thefuck import const
from thefuck.exceptions import EmptyCommand
@@ -39,19 +40,20 @@ class TestRule(object):
enabled_by_default=True,
priority=900,
requires_output=True))
assert Rule.from_path(Path('/rules/bash.py')) \
rule_path = os.path.join(os.sep, 'rules', 'bash.py')
assert Rule.from_path(Path(rule_path)) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
load_source.assert_called_once_with('bash', rule_path)
@pytest.mark.parametrize('rules, exclude_rules, rule, is_enabled', [
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
(const.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
(['git'], [], Rule('git', enabled_by_default=False), True),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
([], ['git'], Rule('git', enabled_by_default=True), False),
([], ['git'], Rule('git', enabled_by_default=False), False)])
def test_is_enabled(self, settings, rules, exclude_rules, rule, is_enabled):
@@ -103,11 +105,6 @@ class TestCommand(object):
monkeypatch.setattr('thefuck.types.Command._wait_output',
staticmethod(lambda *_: True))
@pytest.fixture(autouse=True)
def generic_shell(self, monkeypatch):
monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x)
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_from_script_calls(self, Popen, settings):
settings.env = {}
assert Command.from_raw_script(
@@ -115,6 +112,7 @@ class TestCommand(object):
'apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
env={})

View File

@@ -31,7 +31,9 @@ def test_read_actions(patch_get_key):
# Ctrl+C:
const.KEY_CTRL_C])
assert list(islice(ui.read_actions(), 5)) \
== [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT]
== [const.ACTION_SELECT, const.ACTION_SELECT,
const.ACTION_PREVIOUS, const.ACTION_NEXT,
const.ACTION_ABORT]
def test_command_selector():

View File

@@ -3,7 +3,8 @@ from mock import Mock
import six
from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, compatibility_call
get_all_matched_commands, is_app, for_app, cache, compatibility_call, \
get_valid_history_without_current
from tests.utils import Command
@@ -50,7 +51,7 @@ class TestGetClosest(object):
@pytest.fixture
def get_aliases(mocker):
mocker.patch('thefuck.shells.get_aliases',
mocker.patch('thefuck.shells.shell.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
@@ -162,10 +163,10 @@ class TestCache(object):
@pytest.fixture
def key(self):
if six.PY3:
return 'tests.test_utils.<function TestCache.fn.<locals>.fn '
else:
if six.PY2:
return 'tests.test_utils.<function fn '
else:
return 'tests.test_utils.<function TestCache.fn.<locals>.fn '
def test_with_blank_cache(self, shelve, fn, key):
assert shelve == {}
@@ -229,3 +230,29 @@ class TestCompatibilityCall(object):
return True
assert compatibility_call(side_effect, Command(), Command())
class TestGetValidHistoryWithoutCurrent(object):
@pytest.fixture(autouse=True)
def history(self, mocker):
return mocker.patch('thefuck.shells.shell.get_history',
return_value=['le cat', 'fuck', 'ls cat',
'diff x', 'nocommand x'])
@pytest.fixture(autouse=True)
def alias(self, mocker):
return mocker.patch('thefuck.utils.get_alias',
return_value='fuck')
@pytest.fixture(autouse=True)
def callables(self, mocker):
return mocker.patch('thefuck.utils.get_all_executables',
return_value=['diff', 'ls'])
@pytest.mark.parametrize('script, result', [
('le cat', ['ls cat', 'diff x']),
('diff x', ['ls cat']),
('fuck', ['ls cat', 'diff x'])])
def test_get_valid_history_without_current(self, script, result):
command = Command(script=script)
assert get_valid_history_without_current(command) == result

View File

@@ -1,5 +1,5 @@
from thefuck import types
from thefuck.conf import DEFAULT_PRIORITY
from thefuck.const import DEFAULT_PRIORITY
class Command(types.Command):

View File

@@ -3,44 +3,7 @@ import os
import sys
from pathlib import Path
from six import text_type
ALL_ENABLED = object()
DEFAULT_RULES = [ALL_ENABLED]
DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'exclude_rules': [],
'wait_command': 3,
'require_confirmation': True,
'no_colors': False,
'debug': False,
'priority': {},
'history_limit': None,
'alter_history': True,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_EXCLUDE_RULES': 'exclude_rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_DEBUG': 'debug',
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history'}
SETTINGS_HEADER = u"""# The Fuck settings file
#
# The rules are defined as in the example bellow:
#
# rules = ['cd_parent', 'git_push', 'python_command', 'sudo']
#
# The default values are as follows. Uncomment and change to fit your needs.
# See https://github.com/nvbn/thefuck#settings for more information.
#
"""
from . import const
class Settings(dict):
@@ -71,8 +34,8 @@ class Settings(dict):
settings_path = self.user_dir.joinpath('settings.py')
if not settings_path.is_file():
with settings_path.open(mode='w') as settings_file:
settings_file.write(SETTINGS_HEADER)
for setting in DEFAULT_SETTINGS.items():
settings_file.write(const.SETTINGS_HEADER)
for setting in const.DEFAULT_SETTINGS.items():
settings_file.write(u'# {} = {}\n'.format(*setting))
def _get_user_dir_path(self):
@@ -100,14 +63,14 @@ class Settings(dict):
settings = load_source(
'settings', text_type(self.user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in DEFAULT_SETTINGS.keys()
for key in const.DEFAULT_SETTINGS.keys()
if hasattr(settings, key)}
def _rules_from_env(self, val):
"""Transforms rules list from env-string to python."""
val = val.split(':')
if 'DEFAULT_RULES' in val:
val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
val = const.DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
return val
def _priority_from_env(self, val):
@@ -139,8 +102,8 @@ class Settings(dict):
def _settings_from_env(self):
"""Loads settings from env."""
return {attr: self._val_from_env(env, attr)
for env, attr in ENV_TO_ATTR.items()
for env, attr in const.ENV_TO_ATTR.items()
if env in os.environ}
settings = Settings(DEFAULT_SETTINGS)
settings = Settings(const.DEFAULT_SETTINGS)

View File

@@ -12,3 +12,45 @@ class _GenConst(object):
KEY_UP = _GenConst('')
KEY_DOWN = _GenConst('')
KEY_CTRL_C = _GenConst('Ctrl+C')
ACTION_SELECT = _GenConst('select')
ACTION_ABORT = _GenConst('abort')
ACTION_PREVIOUS = _GenConst('previous')
ACTION_NEXT = _GenConst('next')
ALL_ENABLED = _GenConst('All rules enabled')
DEFAULT_RULES = [ALL_ENABLED]
DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'exclude_rules': [],
'wait_command': 3,
'require_confirmation': True,
'no_colors': False,
'debug': False,
'priority': {},
'history_limit': None,
'alter_history': True,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_EXCLUDE_RULES': 'exclude_rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_DEBUG': 'debug',
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history'}
SETTINGS_HEADER = u"""# The Fuck settings file
#
# The rules are defined as in the example bellow:
#
# rules = ['cd_parent', 'git_push', 'python_command', 'sudo']
#
# The default values are as follows. Uncomment and change to fit your needs.
# See https://github.com/nvbn/thefuck#settings for more information.
#
"""

View File

@@ -7,11 +7,12 @@ from argparse import ArgumentParser
from warnings import warn
from pprint import pformat
import sys
from . import logs, types, shells
from . import logs, types
from .shells import shell
from .conf import settings
from .corrector import get_corrected_commands
from .exceptions import EmptyCommand
from .utils import get_installation_info
from .utils import get_installation_info, get_alias
from .ui import select_command
@@ -42,10 +43,10 @@ def print_alias(entry_point=True):
else:
position = 2
alias = shells.thefuck_alias()
alias = get_alias()
if len(sys.argv) > position:
alias = sys.argv[position]
print(shells.app_alias(alias))
print(shell.app_alias(alias))
def how_to_configure_alias():
@@ -55,17 +56,17 @@ def how_to_configure_alias():
"""
settings.init()
logs.how_to_configure_alias(shells.how_to_configure())
logs.how_to_configure_alias(shell.how_to_configure())
def main():
parser = ArgumentParser(prog='thefuck')
version = get_installation_info().version
parser.add_argument(
'-v', '--version',
action='version',
version='The Fuck {} using Python {}'.format(
version, sys.version.split()[0]))
'-v', '--version',
action='version',
version='The Fuck {} using Python {}'.format(
version, sys.version.split()[0]))
parser.add_argument('-a', '--alias',
action='store_true',
help='[custom-alias-name] prints alias for current shell')

View File

@@ -1,8 +1,10 @@
from thefuck import shells
from thefuck.specific.apt import apt_available
from thefuck.utils import memoize
from thefuck.shells import shell
try:
import CommandNotFound
enabled_by_default = apt_available
except ImportError:
enabled_by_default = False
@@ -26,5 +28,5 @@ def match(command):
def get_new_command(command):
name = get_package(command.script)
formatme = shells.and_('sudo apt-get install {}', '{}')
formatme = shell.and_('sudo apt-get install {}', '{}')
return formatme.format(name, command.script)

View File

@@ -1,6 +1,9 @@
import re
from thefuck.specific.apt import apt_available
from thefuck.utils import for_app
enabled_by_default = apt_available
@for_app('apt-get')
def match(command):

View File

@@ -1,7 +1,10 @@
import subprocess
from thefuck.specific.apt import apt_available
from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app, eager, replace_command
enabled_by_default = apt_available
@for_app('apt', 'apt-get', 'apt-cache')
@sudo_support

View File

@@ -1,10 +1,9 @@
import os
import re
from thefuck.utils import get_closest, replace_argument, which
from thefuck.specific.brew import get_brew_path_prefix
from thefuck.utils import get_closest, replace_argument
from thefuck.specific.brew import get_brew_path_prefix, brew_available
enabled_by_default = bool(which('brew'))
enabled_by_default = brew_available
def _get_formulas():

View File

@@ -1,12 +1,14 @@
import os
import re
from thefuck.utils import get_closest, replace_command
from thefuck.specific.brew import get_brew_path_prefix
from thefuck.specific.brew import get_brew_path_prefix, brew_available
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
TAP_CMD_PATH = '/%s/%s/cmd'
enabled_by_default = brew_available
def _get_brew_commands(brew_path_prefix):
"""To get brew default commands on local environment"""
@@ -53,7 +55,7 @@ def _brew_commands():
if brew_path_prefix:
try:
return _get_brew_commands(brew_path_prefix) \
+ _get_brew_tap_specific_commands(brew_path_prefix)
+ _get_brew_tap_specific_commands(brew_path_prefix)
except OSError:
pass

View File

@@ -0,0 +1,12 @@
from thefuck.utils import for_app
@for_app('brew', at_least=2)
def match(command):
return ('update' in command.script
and "Error: This command updates brew itself" in command.stderr
and "Use 'brew upgrade <formula>'" in command.stderr)
def get_new_command(command):
return command.script.replace('update', 'upgrade')

View File

@@ -4,6 +4,9 @@
# > brew upgrade
# Warning: brew upgrade with no arguments will change behaviour soon!
# It currently upgrades all formula but this will soon change to require '--all'.
from thefuck.specific.brew import brew_available
enabled_by_default = brew_available
def match(command):

View File

@@ -1,7 +1,7 @@
import re
from thefuck import shells
from thefuck.utils import for_app
from thefuck.specific.sudo import sudo_support
from thefuck.shells import shell
@sudo_support
@@ -14,5 +14,5 @@ def match(command):
@sudo_support
def get_new_command(command):
repl = shells.and_('mkdir -p \\1', 'cd \\1')
repl = shell.and_('mkdir -p \\1', 'cd \\1')
return re.sub(r'^cd (.*)', repl, command.script)

15
thefuck/rules/chmod_x.py Normal file
View File

@@ -0,0 +1,15 @@
import os
from thefuck.shells import shell
def match(command):
return (command.script.startswith('./')
and 'permission denied' in command.stderr.lower()
and os.path.exists(command.script_parts[0])
and not os.access(command.script_parts[0], os.X_OK))
def get_new_command(command):
return shell.and_(
'chmod +x {}'.format(command.script_parts[0][2:]),
command.script)

View File

@@ -1,7 +1,7 @@
import tarfile
import os
from thefuck import shells
from thefuck.utils import for_app
from thefuck.shells import shell
tar_extensions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
@@ -33,8 +33,8 @@ def match(command):
def get_new_command(command):
dir = shells.quote(_tar_file(command.script_parts)[1])
return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
dir = shell.quote(_tar_file(command.script_parts)[1])
return shell.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
.format(dir=dir, cmd=command.script)

View File

@@ -1,7 +1,7 @@
import os
import zipfile
from thefuck.utils import for_app
from thefuck.shells import quote
from thefuck.shells import shell
def _is_bad_zip(file):
@@ -38,7 +38,8 @@ def match(command):
def get_new_command(command):
return u'{} -d {}'.format(command.script, quote(_zip_file(command)[:-4]))
return u'{} -d {}'.format(
command.script, shell.quote(_zip_file(command)[:-4]))
def side_effect(old_cmd, command):

View File

@@ -2,7 +2,7 @@ import re
import os
from thefuck.utils import memoize, default_settings
from thefuck.conf import settings
from thefuck import shells
from thefuck.shells import shell
# order is important: only the first match is considered
@@ -75,4 +75,4 @@ def get_new_command(command):
file=m.group('file'),
line=m.group('line'))
return shells.and_(editor_call, command.script)
return shell.and_(editor_call, command.script)

View File

@@ -1,19 +1,18 @@
import re
from thefuck import shells
from thefuck.shells import shell
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr)
return 'did not match any file(s) known to git.' in command.stderr
@git_support
def get_new_command(command):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
r"did not match any file\(s\) known to git.", command.stderr)[0]
r"error: pathspec '([^']*)' "
r'did not match any file\(s\) known to git.', command.stderr)[0]
formatme = shells.and_('git add -- {}', '{}')
formatme = shell.and_('git add -- {}', '{}')
return formatme.format(missing_file, command.script)

View File

@@ -0,0 +1,23 @@
import re
from thefuck.shells import shell
from thefuck.specific.git import git_support
from thefuck.utils import eager
@git_support
def match(command):
return ('branch' in command.script
and "fatal: A branch named '" in command.stderr
and " already exists." in command.stderr)
@git_support
@eager
def get_new_command(command):
branch_name = re.findall(
r"fatal: A branch named '([^']*)' already exists.", command.stderr)[0]
new_command_templates = [['git branch -d {0}', 'git branch {0}'],
['git branch -D {0}', 'git branch {0}'],
['git checkout {0}']]
for new_command_template in new_command_templates:
yield shell.and_(*new_command_template).format(branch_name)

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.specific.git import git_support
@@ -11,4 +11,4 @@ def match(command):
@git_support
def get_new_command(command):
return shells.and_('git branch --delete list', 'git branch')
return shell.and_('git branch --delete list', 'git branch')

View File

@@ -1,8 +1,9 @@
import re
import subprocess
from thefuck import shells, utils
from thefuck import utils
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
from thefuck.shells import shell
@git_support
@@ -34,5 +35,5 @@ def get_new_command(command):
if closest_branch:
return replace_argument(command.script, missing_file, closest_branch)
else:
return shells.and_('git branch {}', '{}').format(
return shell.and_('git branch {}', '{}').format(
missing_file, command.script)

View File

@@ -0,0 +1,12 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return 'help' in command.script and ' is aliased to ' in command.stdout
@git_support
def get_new_command(command):
aliased = command.stdout.split('`', 2)[2].split("'", 1)[0].split(' ', 1)[0]
return 'git help {}'.format(aliased)

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.specific.git import git_support
@@ -14,4 +14,4 @@ def get_new_command(command):
branch = line.split(' ')[-1]
set_upstream = line.replace('<remote>', 'origin')\
.replace('<branch>', branch)
return shells.and_(set_upstream, command.script)
return shell.and_(set_upstream, command.script)

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@@ -13,5 +13,5 @@ def match(command):
@git_support
def get_new_command(command):
return shells.and_(replace_argument(command.script, 'push', 'pull'),
command.script)
return shell.and_(replace_argument(command.script, 'push', 'pull'),
command.script)

View File

@@ -0,0 +1,14 @@
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('set-url' in command.script and 'fatal: No such remote' in command.stderr)
def get_new_command(command):
return replace_argument(command.script, 'set-url', 'add')
enabled_by_default = True

View File

@@ -0,0 +1,15 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return (' rm ' in command.script
and "fatal: not removing '" in command.stderr
and "' recursively without -r" in command.stderr)
@git_support
def get_new_command(command):
index = command.script_parts.index('rm') + 1
command.script_parts.insert(index, '-r')
return u' '.join(command.script_parts)

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.specific.git import git_support
@@ -11,5 +11,5 @@ def match(command):
@git_support
def get_new_command(command):
formatme = shells.and_('git stash', '{}')
formatme = shell.and_('git stash', '{}')
return formatme.format(command.script)

View File

@@ -0,0 +1,23 @@
import os
from thefuck.utils import for_app
def _get_actual_file(parts):
for part in parts[1:]:
if os.path.isfile(part) or os.path.isdir(part):
return part
@for_app('grep', 'egrep')
def match(command):
return ': No such file or directory' in command.stderr \
and _get_actual_file(command.script_parts)
def get_new_command(command):
actual_file = _get_actual_file(command.script_parts)
parts = command.script_parts[::]
# Moves file to the end of the script:
parts.remove(actual_file)
parts.append(actual_file)
return ' '.join(parts)

View File

@@ -1,37 +1,15 @@
from difflib import get_close_matches
from thefuck.shells import get_history, thefuck_alias
from thefuck.utils import get_closest, memoize, get_all_executables
def _not_corrected(history, tf_alias):
"""Returns all lines from history except that comes before `fuck`."""
previous = None
for line in history:
if previous is not None and line != tf_alias:
yield previous
previous = line
if history:
yield history[-1]
@memoize
def _history_of_exists_without_current(command):
history = get_history()
tf_alias = thefuck_alias()
executables = get_all_executables()
return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script
and line.split(' ')[0] in executables]
from thefuck.utils import get_closest, get_valid_history_without_current
def match(command):
return len(get_close_matches(command.script,
_history_of_exists_without_current(command)))
get_valid_history_without_current(command)))
def get_new_command(command):
return get_closest(command.script,
_history_of_exists_without_current(command))
get_valid_history_without_current(command))
priority = 9999

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""Suggest creating symbolic link if hard link is not allowed.
Example:
> ln barDir barLink
ln: barDir: hard link not allowed for directory
--> ln -s barDir barLink
"""
import re
from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return (command.stderr.endswith("hard link not allowed for directory") and
command.script.startswith("ln "))
@sudo_support
def get_new_command(command):
return re.sub(r'^ln ', 'ln -s ', command.script)

View File

@@ -1,5 +1,6 @@
from difflib import get_close_matches
from thefuck.utils import get_all_executables
from thefuck.utils import get_all_executables, \
get_valid_history_without_current, get_closest
from thefuck.specific.sudo import sudo_support
@@ -11,10 +12,29 @@ def match(command):
get_all_executables())))
def _get_used_executables(command):
for script in get_valid_history_without_current(command):
yield script.split(' ')[0]
@sudo_support
def get_new_command(command):
old_command = command.script_parts[0]
new_cmds = get_close_matches(old_command, get_all_executables(), cutoff=0.1)
# One from history:
already_used = get_closest(
old_command, _get_used_executables(command),
fallback_to_first=False)
if already_used:
new_cmds = [already_used]
else:
new_cmds = []
# Other from all executables:
new_cmds += [cmd for cmd in get_close_matches(old_command,
get_all_executables())
if cmd not in new_cmds]
return [' '.join([new_command] + command.script_parts[1:])
for new_command in new_cmds]

View File

@@ -1,5 +1,5 @@
import re
from thefuck import shells
from thefuck.shells import shell
patterns = (
@@ -26,5 +26,5 @@ def get_new_command(command):
file = file[0]
dir = file[0:file.rfind('/')]
formatme = shells.and_('mkdir -p {}', '{}')
formatme = shell.and_('mkdir -p {}', '{}')
return formatme.format(dir, command.script)

View File

@@ -0,0 +1,39 @@
from thefuck.utils import replace_argument, for_app, eager, get_closest
from thefuck.specific.sudo import sudo_support
def _get_wrong_command(script_parts):
commands = [part for part in script_parts[1:] if not part.startswith('-')]
if commands:
return commands[0]
@sudo_support
@for_app('npm')
def match(command):
return (command.script_parts[0] == 'npm' and
'where <command> is one of:' in command.stdout and
_get_wrong_command(command.script_parts))
@eager
def _get_available_commands(stdout):
commands_listing = False
for line in stdout.split('\n'):
if line.startswith('where <command> is one of:'):
commands_listing = True
elif commands_listing:
if not line:
break
for command in line.split(', '):
stripped = command.strip()
if stripped:
yield stripped
def get_new_command(command):
npm_commands = _get_available_commands(command.stdout)
wrong_command = _get_wrong_command(command.script_parts)
fixed = get_closest(wrong_command, npm_commands)
return replace_argument(command.script, wrong_command, fixed)

View File

@@ -1,5 +1,5 @@
from thefuck.specific.archlinux import get_pkgfile, archlinux_env
from thefuck import shells
from thefuck.shells import shell
def match(command):
@@ -9,7 +9,7 @@ def match(command):
def get_new_command(command):
packages = get_pkgfile(command.script)
formatme = shells.and_('{} -S {}', '{}')
formatme = shell.and_('{} -S {}', '{}')
return [formatme.format(pacman, package, command.script)
for package in packages]

View File

@@ -1,5 +1,5 @@
import shlex
from thefuck.shells import quote
from thefuck.shells import shell
from thefuck.utils import for_app
@@ -15,4 +15,4 @@ def get_new_command(command):
if e.startswith(('s/', '-es/')) and e[-1] != '/':
script[i] += '/'
return ' '.join(map(quote, script))
return ' '.join(map(shell.quote, script))

View File

@@ -1,12 +1,12 @@
patterns = ['permission denied',
'EACCES',
'pkg: Insufficient privileges',
'eacces',
'pkg: insufficient privileges',
'you cannot perform this operation unless you are root',
'non-root users cannot',
'Operation not permitted',
'operation not permitted',
'root privilege',
'This command has to be run under the root user.',
'This operation requires root.',
'this command has to be run under the root user.',
'this operation requires root.',
'requested operation requires superuser privilege',
'must be run as root',
'must run as root',
@@ -14,10 +14,12 @@ patterns = ['permission denied',
'must be root',
'need to be root',
'need root',
'needs to be run as root',
'only root can ',
'You don\'t have access to the history DB.',
'you don\'t have access to the history db.',
'authentication is required',
'eDSPermissionError']
'edspermissionerror',
'you don\'t have write permissions']
def match(command):
@@ -25,8 +27,8 @@ def match(command):
return False
for pattern in patterns:
if pattern.lower() in command.stderr.lower()\
or pattern.lower() in command.stdout.lower():
if pattern in command.stderr.lower()\
or pattern in command.stdout.lower():
return True
return False

View File

@@ -1,6 +1,5 @@
# -*- encoding: utf-8 -*-
from thefuck.shells import thefuck_alias
from thefuck.utils import memoize
from thefuck.utils import memoize, get_alias
target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''
@@ -35,7 +34,7 @@ def match(command):
return False
matched_layout = _get_matched_layout(command)
return matched_layout and \
_switch_command(command, matched_layout) != thefuck_alias()
_switch_command(command, matched_layout) != get_alias()
def get_new_command(command):

View File

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

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.utils import for_app
@@ -9,4 +9,4 @@ def match(command):
def get_new_command(command):
return shells.and_('tsuru login', command.script)
return shell.and_('tsuru login', command.script)

View File

@@ -1,4 +1,4 @@
from thefuck import shells
from thefuck.shells import shell
from thefuck.utils import for_app
@@ -13,8 +13,8 @@ def get_new_command(command):
if len(cmds) >= 3:
machine = cmds[2]
startAllInstances = shells.and_("vagrant up", command.script)
startAllInstances = shell.and_("vagrant up", command.script)
if machine is None:
return startAllInstances
else:
return [shells.and_("vagrant up " + machine, command.script), startAllInstances]
return [shell.and_("vagrant up " + machine, command.script), startAllInstances]

View File

@@ -1,334 +0,0 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and
`get_aliases` methods.
"""
from collections import defaultdict
from psutil import Process
from subprocess import Popen, PIPE
from time import time
import io
import os
import shlex
import sys
import six
from .utils import DEVNULL, memoize, cache
from .conf import settings
from . import logs
class Generic(object):
def get_aliases(self):
return {}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return command_script.replace(binary, aliases[binary], 1)
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def to_shell(self, command_script):
"""Prepares command for running in shell."""
return command_script
def app_alias(self, fuck):
return "alias {0}='TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
def _get_history_line(self, command_script):
return ''
def put_to_history(self, command_script):
"""Puts command script to shell history."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history:
entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)
def get_history(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history_file:
lines = history_file.readlines()
if settings.history_limit:
lines = lines[-settings.history_limit:]
for line in lines:
prepared = self._script_from_history(line) \
.strip()
if prepared:
yield prepared
def and_(self, *commands):
return u' && '.join(commands)
def how_to_configure(self):
return
def split_command(self, command):
"""Split the command using shell-like syntax."""
if six.PY2:
return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))]
return shlex.split(command)
def quote(self, s):
"""Return a shell-escaped version of the string s."""
if six.PY2:
from pipes import quote
else:
from shlex import quote
return quote(s)
def _script_from_history(self, line):
return line
class Bash(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0} alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1));" \
" history -r'".format(fuck)
def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
@memoize
@cache('.bashrc', '.bash_profile')
def get_aliases(self):
proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.bash_history'))
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
def how_to_configure(self):
if os.path.join(os.path.expanduser('~'), '.bashrc'):
config = '~/.bashrc'
elif os.path.join(os.path.expanduser('~'), '.bash_profile'):
config = '~/.bashrc'
else:
config = 'bash config'
return 'eval $(thefuck --alias)', config
class Fish(Generic):
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
return [alias.strip() for alias in overridden_aliases.split(',')]
else:
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self, fuck):
return ('function {0} -d "Correct your previous console command"\n'
' set -l exit_code $status\n'
' set -l fucked_up_command $history[1]\n'
' env TF_ALIAS={0} PYTHONIOENCODING=utf-8'
' thefuck $fucked_up_command | read -l unfucked_command\n'
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n'
' if test $exit_code -ne 0\n'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' return 0\n'
' end\n'
' end\n'
'end').format(fuck)
@memoize
def get_aliases(self):
overridden = self._get_overridden_aliases()
proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL)
functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return u'fish -ic "{}"'.format(command_script.replace('"', r'\"'))
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def _get_history_file_name(self):
return os.path.expanduser('~/.config/fish/fish_history')
def _get_history_line(self, command_script):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def _script_from_history(self, line):
if '- cmd: ' in line:
return line.split('- cmd: ', 1)[1]
else:
return ''
def and_(self, *commands):
return u'; and '.join(commands)
def how_to_configure(self):
return 'eval thefuck --alias', '~/.config/fish/config.fish'
class Zsh(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0}" \
" alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'".format(fuck)
def _parse_alias(self, alias):
name, value = alias.split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
@memoize
@cache('.zshrc')
def get_aliases(self):
proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.zsh_history'))
def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script)
def _script_from_history(self, line):
if ';' in line:
return line.split(';', 1)[1]
else:
return ''
def how_to_configure(self):
return 'eval $(thefuck --alias)', '~/.zshrc'
class Tcsh(Generic):
def app_alias(self, fuck):
return ("alias {0} 'setenv TF_ALIAS {0} && "
"set fucked_cmd=`history -h 2 | head -n 1` && "
"eval `thefuck ${{fucked_cmd}}`'").format(fuck)
def _parse_alias(self, alias):
name, value = alias.split("\t", 1)
return name, value
@memoize
def get_aliases(self):
proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.history'))
def _get_history_line(self, command_script):
return u'#+{}\n{}\n'.format(int(time()), command_script)
def how_to_configure(self):
return 'eval `thefuck --alias`', '~/.tcshrc'
shells = defaultdict(Generic, {
'bash': Bash(),
'fish': Fish(),
'zsh': Zsh(),
'csh': Tcsh(),
'tcsh': Tcsh()})
@memoize
def _get_shell():
try:
shell = Process(os.getpid()).parent().name()
except TypeError:
shell = Process(os.getpid()).parent.name
return shells[shell]
def from_shell(command):
return _get_shell().from_shell(command)
def to_shell(command):
return _get_shell().to_shell(command)
def app_alias(alias):
return _get_shell().app_alias(alias)
def thefuck_alias():
return os.environ.get('TF_ALIAS', 'fuck')
def put_to_history(command):
try:
return _get_shell().put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
def and_(*commands):
return _get_shell().and_(*commands)
def get_aliases():
return list(_get_shell().get_aliases().keys())
def split_command(command):
return _get_shell().split_command(command)
def quote(s):
return _get_shell().quote(s)
@memoize
def get_history():
return list(_get_shell().get_history())
def how_to_configure():
return _get_shell().how_to_configure()

View File

@@ -0,0 +1,44 @@
"""Package with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and
`get_aliases` methods.
"""
import os
from psutil import Process
from .bash import Bash
from .fish import Fish
from .generic import Generic
from .tcsh import Tcsh
from .zsh import Zsh
from .powershell import Powershell
shells = {'bash': Bash,
'fish': Fish,
'zsh': Zsh,
'csh': Tcsh,
'tcsh': Tcsh,
'powershell': Powershell}
def _get_shell():
proc = Process(os.getpid())
while proc is not None:
try:
name = proc.name()
except TypeError:
name = proc.name
name = os.path.splitext(name)[0]
if name in shells:
return shells[name]()
try:
proc = proc.parent()
except TypeError:
proc = proc.parent
return Generic()
shell = _get_shell()

47
thefuck/shells/bash.py Normal file
View File

@@ -0,0 +1,47 @@
import os
from ..conf import settings
from ..utils import memoize
from .generic import Generic
class Bash(Generic):
def app_alias(self, fuck):
# It is VERY important to have the variables declared WITHIN the alias
alias = "alias {0}='TF_CMD=$(TF_ALIAS={0}" \
" PYTHONIOENCODING=utf-8" \
" TF_SHELL_ALIASES=$(alias)" \
" thefuck $(fc -ln -1)) &&" \
" eval $TF_CMD".format(fuck)
if settings.alter_history:
return alias + " && history -s $TF_CMD'"
else:
return alias + "'"
def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
@memoize
def get_aliases(self):
raw_aliases = os.environ.get('TF_SHELL_ALIASES', '').split('\n')
return dict(self._parse_alias(alias)
for alias in raw_aliases if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.bash_history'))
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
def how_to_configure(self):
if os.path.join(os.path.expanduser('~'), '.bashrc'):
config = '~/.bashrc'
elif os.path.join(os.path.expanduser('~'), '.bash_profile'):
config = '~/.bashrc'
else:
config = 'bash config'
return 'eval $(thefuck --alias)', config

87
thefuck/shells/fish.py Normal file
View File

@@ -0,0 +1,87 @@
from subprocess import Popen, PIPE
from time import time
import os
import sys
import six
from .. import logs
from ..utils import DEVNULL, memoize, cache
from .generic import Generic
class Fish(Generic):
def _get_overridden_aliases(self):
overridden = os.environ.get('THEFUCK_OVERRIDDEN_ALIASES',
os.environ.get('TF_OVERRIDDEN_ALIASES', ''))
default = {'cd', 'grep', 'ls', 'man', 'open'}
for alias in overridden.split(','):
default.add(alias.strip())
return default
def app_alias(self, fuck):
# It is VERY important to have the variables declared WITHIN the alias
return ('function {0} -d "Correct your previous console command"\n'
' set -l fucked_up_command $history[1]\n'
' env TF_ALIAS={0} PYTHONIOENCODING=utf-8'
' thefuck $fucked_up_command | read -l unfucked_command\n'
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' end\n'
'end').format(fuck)
@memoize
@cache('.config/fish/config.fish', '.config/fish/functions')
def get_aliases(self):
overridden = self._get_overridden_aliases()
proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL)
functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return u'fish -ic "{}"'.format(command_script.replace('"', r'\"'))
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def _get_history_file_name(self):
return os.path.expanduser('~/.config/fish/fish_history')
def _get_history_line(self, command_script):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def _script_from_history(self, line):
if '- cmd: ' in line:
return line.split('- cmd: ', 1)[1]
else:
return ''
def and_(self, *commands):
return u'; and '.join(commands)
def how_to_configure(self):
return (r"eval (thefuck --alias | tr '\n' ';')",
'~/.config/fish/config.fish')
def put_to_history(self, command):
try:
return self._put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
def _put_to_history(self, command_script):
"""Puts command script to shell history."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history:
entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)

91
thefuck/shells/generic.py Normal file
View File

@@ -0,0 +1,91 @@
import io
import os
import shlex
import six
from ..utils import memoize
from ..conf import settings
class Generic(object):
def get_aliases(self):
return {}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return command_script.replace(binary, aliases[binary], 1)
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def to_shell(self, command_script):
"""Prepares command for running in shell."""
return command_script
def app_alias(self, fuck):
return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
def _get_history_line(self, command_script):
return ''
@memoize
def get_history(self):
return list(self._get_history_lines())
def _get_history_lines(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history_file:
lines = history_file.readlines()
if settings.history_limit:
lines = lines[-settings.history_limit:]
for line in lines:
prepared = self._script_from_history(line) \
.strip()
if prepared:
yield prepared
def and_(self, *commands):
return u' && '.join(commands)
def how_to_configure(self):
return
def split_command(self, command):
"""Split the command using shell-like syntax."""
if six.PY2:
return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))]
return shlex.split(command)
def quote(self, s):
"""Return a shell-escaped version of the string s."""
if six.PY2:
from pipes import quote
else:
from shlex import quote
return quote(s)
def _script_from_history(self, line):
return line
def put_to_history(self, command):
"""Adds fixed command to shell history.
In most of shells we change history on shell-level, but not
all shells support it (Fish).
"""

View File

@@ -0,0 +1,18 @@
from .generic import Generic
class Powershell(Generic):
def app_alias(self, fuck):
return 'function ' + fuck + ' { \n' \
' $fuck = $(thefuck (Get-History -Count 1).CommandLine)\n' \
' if (-not [string]::IsNullOrWhiteSpace($fuck)) {\n' \
' if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5) }\n' \
' else { iex "$fuck" }\n' \
' }\n' \
'}\n'
def and_(self, *commands):
return u' -and '.join('({0})'.format(c) for c in commands)
def how_to_configure(self):
return 'iex "thefuck --alias"', '$profile'

34
thefuck/shells/tcsh.py Normal file
View File

@@ -0,0 +1,34 @@
from subprocess import Popen, PIPE
from time import time
import os
from ..utils import DEVNULL, memoize
from .generic import Generic
class Tcsh(Generic):
def app_alias(self, fuck):
return ("alias {0} 'setenv TF_ALIAS {0} && "
"set fucked_cmd=`history -h 2 | head -n 1` && "
"eval `thefuck ${{fucked_cmd}}`'").format(fuck)
def _parse_alias(self, alias):
name, value = alias.split("\t", 1)
return name, value
@memoize
def get_aliases(self):
proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.history'))
def _get_history_line(self, command_script):
return u'#+{}\n{}\n'.format(int(time()), command_script)
def how_to_configure(self):
return 'eval `thefuck --alias`', '~/.tcshrc'

48
thefuck/shells/zsh.py Normal file
View File

@@ -0,0 +1,48 @@
from time import time
import os
from ..conf import settings
from ..utils import memoize
from .generic import Generic
class Zsh(Generic):
def app_alias(self, alias_name):
# It is VERY important to have the variables declared WITHIN the alias
alias = "alias {0}='TF_CMD=$(TF_ALIAS={0}" \
" PYTHONIOENCODING=utf-8" \
" TF_SHELL_ALIASES=$(alias)" \
" thefuck $(fc -ln -1 | tail -n 1)) &&" \
" eval $TF_CMD".format(alias_name)
if settings.alter_history:
return alias + " && print -s $TF_CMD'"
else:
return alias + "'"
def _parse_alias(self, alias):
name, value = alias.split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
@memoize
def get_aliases(self):
raw_aliases = os.environ.get('TF_SHELL_ALIASES', '').split('\n')
return dict(self._parse_alias(alias)
for alias in raw_aliases if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.zsh_history'))
def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script)
def _script_from_history(self, line):
if ';' in line:
return line.split(';', 1)[1]
else:
return ''
def how_to_configure(self):
return 'eval $(thefuck --alias)', '~/.zshrc'

3
thefuck/specific/apt.py Normal file
View File

@@ -0,0 +1,3 @@
from thefuck.utils import which
apt_available = bool(which('apt-get'))

View File

@@ -2,7 +2,7 @@ import subprocess
from ..utils import memoize, which
enabled_by_default = bool(which('brew'))
brew_available = bool(which('brew'))
@memoize

View File

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

View File

@@ -1,3 +1,4 @@
import os
import sys
import msvcrt
import win_unicode_console
@@ -16,10 +17,11 @@ def get_key():
ch = msvcrt.getch() # second call returns the actual key code
if ch == b'\x03':
raise const.KEY_CTRL_C
return const.KEY_CTRL_C
if ch == b'H':
return const.KEY_UP
if ch == b'P':
return const.KEY_DOWN
return ch.decode(sys.stdout.encoding)
encoding = sys.stdout.encoding or os.environ.get('PYTHONIOENCODING', 'utf-8')
return ch.decode(encoding)

View File

@@ -4,8 +4,10 @@ import os
import sys
import six
from psutil import Process, TimeoutExpired
from . import logs, shells
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from . import logs
from .shells import shell
from .conf import settings
from .const import DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand
from .utils import compatibility_call
@@ -29,7 +31,7 @@ class Command(object):
def script_parts(self):
if not hasattr(self, '_script_parts'):
try:
self._script_parts = shells.split_command(self.script)
self._script_parts = shell.split_command(self.script)
except Exception:
logs.debug(u"Can't split command script {} because:\n {}".format(
self, sys.exc_info()))
@@ -93,7 +95,7 @@ class Command(object):
script = ' '.join(raw_script)
script = script.strip()
return shells.from_shell(script)
return shell.from_shell(script)
@classmethod
def from_raw_script(cls, raw_script):
@@ -112,7 +114,7 @@ class Command(object):
env.update(settings.env)
with logs.debug_time(u'Call: {}; with env: {};'.format(script, env)):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
result = Popen(script, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
if cls._wait_output(result):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
@@ -279,6 +281,8 @@ class CorrectedCommand(object):
if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
if settings.alter_history:
shells.put_to_history(self.script)
shell.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
logs.debug(u'PYTHONIOENCODING: {}'.format(
os.environ.get('PYTHONIOENCODING', '!!not-set!!')))
print(self.script)

View File

@@ -6,11 +6,6 @@ from .exceptions import NoRuleMatched
from .system import get_key
from . import logs, const
SELECT = 0
ABORT = 1
PREVIOUS = 2
NEXT = 3
def read_actions():
"""Yields actions for pressed keys."""
@@ -18,13 +13,13 @@ def read_actions():
key = get_key()
if key in (const.KEY_UP, 'k'):
yield PREVIOUS
yield const.ACTION_PREVIOUS
elif key in (const.KEY_DOWN, 'j'):
yield NEXT
yield const.ACTION_NEXT
elif key == const.KEY_CTRL_C:
yield ABORT
yield const.ACTION_ABORT
elif key in ('\n', '\r'):
yield SELECT
yield const.ACTION_SELECT
class CommandSelector(object):
@@ -83,15 +78,15 @@ def select_command(corrected_commands):
logs.confirm_text(selector.value)
for action in read_actions():
if action == SELECT:
if action == const.ACTION_SELECT:
sys.stderr.write('\n')
return selector.value
elif action == ABORT:
elif action == const.ACTION_ABORT:
logs.failed('\nAborted')
return
elif action == PREVIOUS:
elif action == const.ACTION_PREVIOUS:
selector.previous()
logs.confirm_text(selector.value)
elif action == NEXT:
elif action == const.ACTION_NEXT:
selector.next()
logs.confirm_text(selector.value)

View File

@@ -1,9 +1,9 @@
import dbm
import os
import pickle
import pkg_resources
import re
import shelve
import six
from .conf import settings
from contextlib import closing
from decorator import decorator
@@ -15,6 +15,13 @@ from warnings import warn
DEVNULL = open(os.devnull, 'w')
if six.PY2:
import anydbm
shelve_open_error = anydbm.error
else:
import dbm
shelve_open_error = dbm.error
def memoize(fn):
"""Caches previous calls to the function."""
@@ -40,22 +47,26 @@ memoize.disabled = False
@memoize
def which(program):
"""Returns `program` path or `None`."""
try:
from shutil import which
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
return which(program)
except ImportError:
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
return None
def default_settings(params):
@@ -87,7 +98,7 @@ def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
@memoize
def get_all_executables():
from thefuck.shells import thefuck_alias, get_aliases
from thefuck.shells import shell
def _safe(fn, fallback):
try:
@@ -95,7 +106,7 @@ def get_all_executables():
except OSError:
return fallback
tf_alias = thefuck_alias()
tf_alias = get_alias()
tf_entry_points = get_installation_info().get_entry_map()\
.get('console_scripts', {})\
.keys()
@@ -104,7 +115,7 @@ def get_all_executables():
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)
and exe.name not in tf_entry_points]
aliases = [alias for alias in get_aliases() if alias != tf_alias]
aliases = [alias for alias in shell.get_aliases() if alias != tf_alias]
return bins + aliases
@@ -217,8 +228,8 @@ def cache(*depends_on):
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
except dbm.error:
# Caused when going from Python 2 to Python 3
except (shelve_open_error, ImportError):
# Caused when switching between Python versions
warn("Removing possibly out-dated cache")
os.remove(cache_path)
@@ -254,3 +265,28 @@ def compatibility_call(fn, *args):
def get_installation_info():
return pkg_resources.require('thefuck')[0]
def get_alias():
return os.environ.get('TF_ALIAS', 'fuck')
@memoize
def get_valid_history_without_current(command):
def _not_corrected(history, tf_alias):
"""Returns all lines from history except that comes before `fuck`."""
previous = None
for line in history:
if previous is not None and line != tf_alias:
yield previous
previous = line
if history:
yield history[-1]
from thefuck.shells import shell
history = shell.get_history()
tf_alias = get_alias()
executables = get_all_executables()
return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script
and line.split(' ')[0] in executables]