1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-03-14 06:38:32 +00:00

Merge branch 'master' of https://github.com/nvbn/thefuck into bugfix/apt_get

This commit is contained in:
David 2020-01-05 16:58:51 +00:00
commit c558438bc2
116 changed files with 2200 additions and 318 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[*.py]
max_line_length = 119

View File

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

View File

@ -1,25 +1,25 @@
language: python
sudo: false
os: linux
dist: xenial
matrix:
include:
- os: linux
dist: trusty
python: "3.6"
- os: linux
dist: trusty
python: "3.5"
- os: linux
dist: trusty
python: "3.4"
- os: linux
dist: trusty
python: "2.7"
- python: "nightly"
- python: "3.8-dev"
- python: "3.8"
- python: "3.7-dev"
- python: "3.7"
- python: "3.6-dev"
- python: "3.6"
- python: "3.5"
- python: "2.7"
- os: osx
env: FORMULA="python"
language: generic
- os: osx
env: FORMULA="python3"
language: generic
allow_failures:
- python: nightly
- python: 3.8-dev
- python: 3.7-dev
- python: 3.6-dev
services:
- docker
addons:
@ -28,9 +28,11 @@ addons:
- python-commandnotfound
- python3-commandnotfound
before_install:
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update ; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then if brew ls --versions $FORMULA; then brew upgrade $FORMULA || echo Python is up to date; else brew install $FORMULA; fi; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p $FORMULA; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then rm -rf /usr/local/include/c++; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew upgrade python; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then pip3 install virtualenv; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p python3; fi
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then source venv/bin/activate; fi
- pip install -U pip
- pip install -U coveralls
@ -42,7 +44,7 @@ script:
- flake8
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys tests"
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi
- if [[ $TRAVIS_PYTHON_VERSION != 3.6 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi
- if [[ $TRAVIS_PYTHON_VERSION == 3.8 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi
- if [[ $TRAVIS_PYTHON_VERSION != 3.8 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi
after_success:
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi
- if [[ $TRAVIS_PYTHON_VERSION == 3.8 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi

View File

@ -26,6 +26,8 @@ fixes, etc.
# Developing
[Create and activate a Python 3 virtual environment.](https://docs.python.org/3/tutorial/venv.html)
Install `The Fuck` for development:
```bash

159
README.md
View File

@ -1,14 +1,15 @@
# 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).
*The Fuck* is a magnificent app, inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320),
that corrects errors in previous console commands.
The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode)
Is *The Fuck* too slow? [Try the experimental instant mode!](#experimental-instant-mode)
[![gif with examples][examples-link]][examples-link]
Few more examples:
More examples:
```bash
➜ apt-get install vim
@ -75,8 +76,8 @@ REPL-y 0.3.1
...
```
If you are not scared to blindly run the changed command, there is a `require_confirmation`
[settings](#settings) option:
If you're not afraid of blindly running corrected commands, the
`require_confirmation` [settings](#settings) option can be disabled:
```bash
➜ apt-get install vim
@ -98,29 +99,40 @@ Reading package lists... Done
## Installation
On OS X you can install `The Fuck` with [Homebrew][homebrew]:
On OS X, you can install *The Fuck* via [Homebrew][homebrew] (or via [Linuxbrew][linuxbrew] on Linux):
```bash
brew install thefuck
```
On Ubuntu you can install `The Fuck` with:
On Ubuntu / Mint, install *The Fuck* with the following commands:
```bash
sudo apt update
sudo apt install python3-dev python3-pip
sudo apt install python3-dev python3-pip python3-setuptools
sudo pip3 install thefuck
```
On other systems you can install `The Fuck` with `pip`:
On FreeBSD, install *The Fuck* with the following commands:
```bash
pkg install thefuck
```
On ChromeOS, install *The Fuck* using [chromebrew](https://github.com/skycocker/chromebrew) with the following command:
```bash
crew install thefuck
```
On other systems, install *The Fuck* by using `pip`:
```bash
pip install thefuck
```
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
[Alternatively, you may use an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
<a href='#manual-installation' name='manual-installation'>#</a>
You should place this command in your `.bash_profile`, `.bashrc`, `.zshrc` or other startup script:
It is recommended that you place this command in your `.bash_profile`,
`.bashrc`, `.zshrc` or other startup script:
```bash
eval $(thefuck --alias)
@ -130,43 +142,47 @@ eval $(thefuck --alias FUCK)
[Or in your shell config (Bash, Zsh, Fish, Powershell, tcsh).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
Changes will be available only in a new shell session.
To make them available immediately, run `source ~/.bashrc` (or your shell config file like `.zshrc`).
Changes are only available in a new shell session. To make changes immediately
available, run `source ~/.bashrc` (or your shell config file like `.zshrc`).
If you want to run fixed command without confirmation you can use `-y` option:
To run fixed commands without confirmation, use the `--yeah` option (or just `-y` for short, or `--hard` if you're especially frustrated):
```bash
fuck -y
fuck --yeah
```
If you want to fix commands recursively until success you can use `-r` option:
To fix commands recursively until succeeding, use the `-r` option:
```bash
fuck -r
```
## Update
## Updating
```bash
pip3 install thefuck --upgrade
```
**Aliases changed in 1.34.**
**Note: Alias functionality was changed in v1.34 of *The Fuck***
## How it works
The Fuck tries to match a rule for the previous command, creates a new command
using the matched rule and runs it. Rules enabled by default are as follows:
*The Fuck* attempts to match the previous command with a rule. If a match is
found, a new command is created using the matched rule and executed. The
following rules are enabled by default:
* `adb_unknown_command` &ndash; fixes misspelled commands like `adb logcta`;
* `ag_literal` &ndash; adds `-Q` to `ag` when suggested;
* `aws_cli` &ndash; fixes misspelled commands like `aws dynamdb scan`;
* `az_cli` &ndash; fixes misspelled commands like `az providers`;
* `cargo` &ndash; runs `cargo build` instead of `cargo`;
* `cargo_no_command` &ndash; fixes wrongs commands like `cargo buid`;
* `cat_dir` &ndash; replaces `cat` with `ls` when you try to `cat` a directory;
* `cd_correction` &ndash; spellchecks and correct failed cd commands;
* `cd_mkdir` &ndash; creates directories before cd'ing into them;
* `cd_parent` &ndash; changes `cd..` to `cd ..`;
* `chmod_x` &ndash; add execution bit;
* `choco_install` &ndash; append common suffixes for chocolatey packages;
* `composer_not_command` &ndash; fixes composer command name;
* `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory;
* `cpp11` &ndash; adds missing `-std=c++11` to `g++` or `clang++`;
@ -174,7 +190,9 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `dirty_unzip` &ndash; fixes `unzip` command that unzipped in the current directory;
* `django_south_ghost` &ndash; adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` &ndash; adds `--merge` to inconsistent django south migration;
* `docker_login` &ndash; executes a `docker login` and repeats the previous command;
* `docker_not_command` &ndash; fixes wrong docker commands like `docker tags`;
* `docker_image_being_used_by_container` &dash; removes the container that is using the image before removing the image;
* `dry` &ndash; fixes repetitions like `git git push`;
* `fab_command_not_found` &ndash; fix misspelled fabric commands;
* `fix_alt_space` &ndash; replaces Alt+Space with Space character;
@ -184,16 +202,19 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_add_force` &ndash; adds `--force` to `git add <pathspec>...` when paths are .gitignore'd;
* `git_bisect_usage` &ndash; fixes `git bisect strt`, `git bisect goood`, `git bisect rset`, etc. when bisecting;
* `git_branch_delete` &ndash; changes `git branch -d` to `git branch -D`;
* `git_branch_delete_checked_out` &ndash; changes `git branch -d` to `git checkout master && git branch -D` when trying to delete a checked out branch;
* `git_branch_exists` &ndash; offers `git branch -d foo`, `git branch -D foo` or `git checkout foo` when creating a branch that already exists;
* `git_branch_list` &ndash; catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` &ndash; fixes branch name or creates new branch;
* `git_commit_amend` &ndash; offers `git commit --amend` after previous commit;
* `git_commit_reset` &ndash; offers `git reset HEAD~` after previous commit;
* `git_diff_no_index` &ndash; adds `--no-index` to previous `git diff` on untracked files;
* `git_diff_staged` &ndash; adds `--staged` to previous `git diff` with unexpected output;
* `git_fix_stash` &ndash; fixes `git stash` commands (misspelled subcommand and missing `save`);
* `git_flag_after_filename` &ndash; fixes `fatal: bad flag '...' after filename`
* `git_help_aliased` &ndash; fixes `git help <alias>` commands replacing <alias> with the aliased command;
* `git_merge` &ndash; adds remote to branch names;
* `git_merge_unrelated` &ndash; adds `--allow-unrelated-histories` when required
* `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;
@ -208,16 +229,17 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_rm_recursive` &ndash; adds `-r` when you try to `rm` a directory;
* `git_rm_staged` &ndash; adds `-f` or `--cached` when you try to `rm` a file with staged changes
* `git_rebase_merge_dir` &ndash; offers `git rebase (--continue | --abort | --skip)` or removing the `.git/rebase-merge` dir when a rebase is in progress;
* `git_remote_seturl_add` &ndash; runs `git remote add` when `git remote set_url` on nonexistant remote;
* `git_remote_seturl_add` &ndash; runs `git remote add` when `git remote set_url` on nonexistent remote;
* `git_stash` &ndash; stashes your local modifications before rebasing or switching branch;
* `git_stash_pop` &ndash; adds your local modifications before popping stash, then resets;
* `git_tag_force` &ndash; adds `--force` to `git tag <tagname>` when the tag already exists;
* `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_unknown_command` &ndash; fixes wrong `go` commands, for example `go bulid`;
* `gradle_no_task` &ndash; fixes not found or ambiguous `gradle` task;
* `gradle_wrapper` &ndash; replaces `gradle` with `./gradlew`;
* `grep_arguments_order` &ndash; fixes grep arguments order for situations like `grep -lir . test`;
* `grep_recursive` &ndash; adds `-r` when you trying to `grep` directory;
* `grep_arguments_order` &ndash; fixes `grep` arguments order for situations like `grep -lir . test`;
* `grep_recursive` &ndash; adds `-r` when you try to `grep` directory;
* `grunt_task_not_found` &ndash; fixes misspelled `grunt` commands;
* `gulp_not_task` &ndash; fixes misspelled `gulp` tasks;
* `has_exists_script` &ndash; prepends `./` when script/binary exists;
@ -229,6 +251,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`;
* `long_form_help` &ndash; changes `-h` to `--help` when the short form version is not supported
* `ln_no_hard_link` &ndash; catches hard link creation on directories, suggest symbolic link;
* `ln_s_order` &ndash; fixes `ln -s` arguments order;
* `ls_all` &ndash; adds `-A` to `ls` when output is empty;
@ -237,26 +260,29 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
* `mercurial` &ndash; fixes wrong `hg` commands;
* `missing_space_before_subcommand` &ndash; fixes command with missing space like `npminstall`;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent;
* `mkdir_p` &ndash; adds `-p` when you try to create a directory without parent;
* `mvn_no_command` &ndash; adds `clean package` to `mvn`;
* `mvn_unknown_lifecycle_phase` &ndash; fixes misspelled lifecycle phases with `mvn`;
* `mvn_unknown_lifecycle_phase` &ndash; fixes misspelled life cycle phases with `mvn`;
* `npm_missing_script` &ndash; fixes `npm` custom script name in `npm run-script <script>`;
* `npm_run_script` &ndash; adds missing `run-script` for custom `npm` scripts;
* `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; either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`;
* `pip_install` &ndash; fixes permission issues with `pip install` commands by adding `--user` or prepending `sudo` if necessary;
* `pip_unknown_command` &ndash; fixes wrong `pip` commands, for example `pip instatl/pip install`;
* `php_s` &ndash; replaces `-s` by `-S` when trying to run a local php server;
* `port_already_in_use` &ndash; kills process that bound port;
* `prove_recursively` &ndash; adds `-r` when called with directory;
* `python_command` &ndash; prepends `python` when you trying to run not executable/without `./` python script;
* `pyenv_no_such_command` &ndash; fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`;
* `python_command` &ndash; prepends `python` when you try to run non-executable/without `./` python script;
* `python_execute` &ndash; appends missing `.py` when executing Python files;
* `quotation_marks` &ndash; fixes uneven usage of `'` and `"` when containing args';
* `path_from_history` &ndash; replaces not found path with similar absolute path from history;
* `react_native_command_unrecognized` &ndash; fixes unrecognized `react-native` commands;
* `remove_trailing_cedilla` &ndash; remove trailling cedillas `ç`, a common typo for european keyboard layouts;
* `rm_dir` &ndash; adds `-rf` when you trying to remove directory;
* `remove_shell_prompt_literal` &ndash; remove leading shell prompt symbol `$`, common when copying commands from documentations;
* `remove_trailing_cedilla` &ndash; remove trailing cedillas `ç`, a common typo for european keyboard layouts;
* `rm_dir` &ndash; adds `-rf` when you try to remove a directory;
* `scm_correction` &ndash; corrects wrong scm like `hg log` to `git log`;
* `sed_unterminated_s` &ndash; adds missing '/' to `sed`'s `s` commands;
* `sl_ls` &ndash; changes `sl` to `ls`;
@ -265,12 +291,14 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `sudo_command_from_user_path` &ndash; runs commands from users `$PATH` with `sudo`;
* `switch_lang` &ndash; switches command from your local layout to en;
* `systemctl` &ndash; correctly orders parameters of confusing `systemctl`;
* `terraform_init.py` &ndash; run `terraform init` before plan or apply;
* `test.py` &ndash; runs `py.test` instead of `test.py`;
* `touch` &ndash; creates missing directories before "touching";
* `tsuru_login` &ndash; runs `tsuru login` if not authenticated or session expired;
* `tsuru_not_command` &ndash; fixes wrong `tsuru` commands like `tsuru shell`;
* `tmux` &ndash; fixes `tmux` commands;
* `unknown_command` &ndash; fixes hadoop hdfs-style "unknown command", for example adds missing '-' to the command on `hdfs dfs ls`;
* `unsudo` &ndash; removes `sudo` from previous command if a process refuses to run on super user privilege.
* `vagrant_up` &ndash; starts up the vagrant instance;
* `whois` &ndash; fixes `whois` command;
* `workon_doesnt_exists` &ndash; fixes `virtualenvwrapper` env name os suggests to create new.
@ -279,7 +307,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `yarn_command_replaced` &ndash; fixes replaced `yarn` commands;
* `yarn_help` &ndash; makes it easier to open `yarn` documentation;
Enabled by default only on specific platforms:
The following rules are enabled by default on specific platforms only:
* `apt_get` &ndash; installs app from apt if it not installed (requires `python-commandnotfound` / `python3-commandnotfound`);
* `apt_get_search` &ndash; changes trying to search using `apt-get` with searching using `apt-cache`;
@ -288,44 +316,51 @@ Enabled by default only on specific platforms:
* `apt_upgrade` &ndash; helps you run `apt upgrade` after `apt list --upgradable`;
* `brew_cask_dependency` &ndash; installs cask dependencies;
* `brew_install` &ndash; fixes formula name for `brew install`;
* `brew_reinstall` &ndash; turns `brew install <formula>` into `brew reinstall <formula>`;
* `brew_link` &ndash; adds `--overwrite --dry-run` if linking fails;
* `brew_uninstall` &ndash; adds `--force` to `brew uninstall` if multiple versions were installed;
* `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_update_formula` &ndash; turns `brew update <formula>` into `brew upgrade <formula>`;
* `dnf_no_such_command` &ndash; fixes mistyped DNF commands;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yaourt` if available);
* `pacman_not_found` &ndash; fixes package name with `pacman` or `yaourt`.
* `nixos_cmd_not_found` &ndash; installs apps on NixOS;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yay` or `yaourt` if available);
* `pacman_not_found` &ndash; fixes package name with `pacman`, `yay` or `yaourt`.
* `yum_invalid_operation` &ndash; fixes invalid `yum` calls, like `yum isntall vim`;
Bundled, but not enabled by default:
The following commands are bundled with *The Fuck*, but are not enabled by
default:
* `git_push_force` &ndash; adds `--force-with-lease` to a `git push` (may conflict with `git_push_pull`);
* `rm_root` &ndash; adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
in `~/.config/thefuck/rules`. The rule should contain two functions:
To add your own rule, create a file named `your-rule-name.py`
in `~/.config/thefuck/rules`. The rule file must contain two functions:
```python
match(command: Command) -> bool
get_new_command(command: Command) -> str | list[str]
```
Also the rule can contain an optional function
Additionally, rules can contain optional functions:
```python
side_effect(old_command: Command, fixed_command: str) -> None
```
and optional `enabled_by_default`, `requires_output` and `priority` variables.
Rules can also contain the optional variables `enabled_by_default`, `requires_output` and `priority`.
`Command` has three attributes: `script`, `output` and `script_parts`.
Rule shouldn't change `Command`.
Your rule should not change `Command`.
*Rules api changed in 3.0:* For accessing settings in rule you need to import it with `from thefuck.conf import settings`.
`settings` is a special object filled with `~/.config/thefuck/settings.py` and values from env ([see more below](#settings)).
**Rules api changed in 3.0:** To access a rule's settings, import it with
`from thefuck.conf import settings`
Simple example of the rule for running script with `sudo`:
`settings` is a special object assembled from `~/.config/thefuck/settings.py`,
and values from env ([see more below](#settings)).
A simple example rule for running a script with `sudo`:
```python
def match(command):
@ -353,9 +388,10 @@ requires_output = True
## Settings
The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME/thefuck/settings.py` (`$XDG_CONFIG_HOME` defaults to `~/.config`):
Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefuck/settings.py`
(`$XDG_CONFIG_HOME` defaults to `~/.config`):
* `rules` &ndash; list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `rules` &ndash; list of enabled rules, by default `thefuck.const.DEFAULT_RULES`;
* `exclude_rules` &ndash; list of disabled rules, by default `[]`;
* `require_confirmation` &ndash; requires confirmation before running new command, by default `True`;
* `wait_command` &ndash; max amount of time in seconds for getting previous command output;
@ -365,9 +401,10 @@ The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME
* `history_limit` &ndash; numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` &ndash; push fixed command to history, by default `True`;
* `wait_slow_command` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `slow_commands` &ndash; list of slow commands.
* `slow_commands` &ndash; list of slow commands;
* `num_close_matches` &ndash; maximum number of close matches to suggest, by default `3`.
Example of `settings.py`:
An example of `settings.py`:
```python
rules = ['sudo', 'no_command']
@ -380,6 +417,7 @@ debug = False
history_limit = 9999
wait_slow_command = 20
slow_commands = ['react-native', 'gradle']
num_close_matches = 5
```
Or via environment variables:
@ -395,7 +433,8 @@ rule with lower `priority` will be matched first;
* `THEFUCK_HISTORY_LIMIT` &ndash; how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` &ndash; push fixed command to history `true/false`;
* `THEFUCK_WAIT_SLOW_COMMAND` &ndash; max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `THEFUCK_SLOW_COMMANDS` &ndash; list of slow commands, like `lein:gradle`.
* `THEFUCK_SLOW_COMMANDS` &ndash; list of slow commands, like `lein:gradle`;
* `THEFUCK_NUM_CLOSE_MATCHES` &ndash; maximum number of close matches to suggest, like `5`.
For example:
@ -407,13 +446,14 @@ export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000'
export THEFUCK_NUM_CLOSE_MATCHES='5'
```
## Third-party packages with rules
If you want to make very specific rules or rules, that you don't want to make public,
but share with other people.
You can create a special package with name `thefuck_contrib_*` with following structure:
If you'd like to make a specific set of non-public rules, but would still like
to share them with others, create a package named `thefuck_contrib_*` with
the following structure:
```
thefuck_contrib_foo
@ -426,20 +466,22 @@ thefuck_contrib_foo
setup.py
```
And thefuck will find all rules from `rules` module.
*The Fuck* will find rules located in the `rules` module.
## Experimental instant mode
By default The Fuck reruns a previous command and that takes time,
in instant mode The Fuck logs output with [script](https://en.wikipedia.org/wiki/Script_(Unix))
and just reads the log.
The default behavior of *The Fuck* requires time to re-run previous commands.
When in instant mode, *The Fuck* saves time by logging output with [script](https://en.wikipedia.org/wiki/Script_(Unix)),
then reading the log.
[![gif with instant mode][instant-mode-gif-link]][instant-mode-gif-link]
At the moment only Python 3 with bash or zsh is supported.
Currently, instant mode only supports Python 3 with bash or zsh. zsh's autocorrect function also needs to be disabled in order for thefuck to work properly.
For enabling instant mode you need to add `--enable-experimental-instant-mode`
to alias initialization in your `.bashrc`, `.bash_profile` or `.zshrc` like:
To enable instant mode, add `--enable-experimental-instant-mode`
to the alias initialization in `.bashrc`, `.bash_profile` or `.zshrc`.
For example:
```bash
eval $(thefuck --alias --enable-experimental-instant-mode)
@ -464,4 +506,5 @@ Project License can be found [here](LICENSE.md).
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
[examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif
[instant-mode-gif-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example_instant_mode.gif
[homebrew]: http://brew.sh/
[homebrew]: https://brew.sh/
[linuxbrew]: https://linuxbrew.sh/

View File

@ -3,9 +3,9 @@ build: false
environment:
matrix:
- PYTHON: "C:/Python27"
- PYTHON: "C:/Python34"
- PYTHON: "C:/Python35"
- PYTHON: "C:/Python36"
- PYTHON: "C:/Python37"
init:
- "ECHO %PYTHON%"

View File

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

View File

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

View File

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

View File

@ -26,12 +26,12 @@ if version < (2, 7):
print('thefuck requires Python version 2.7 or later' +
' ({}.{} detected).'.format(*version))
sys.exit(-1)
elif (3, 0) < version < (3, 4):
print('thefuck requires Python version 3.4 or later' +
elif (3, 0) < version < (3, 5):
print('thefuck requires Python version 3.5 or later' +
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.25'
VERSION = '3.29'
install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte']
extras_require = {':python_version<"3.4"': ['pathlib2'],

25
snapcraft.yaml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,8 @@ from thefuck.types import Command
[('vim', 'main'), ('vim-tiny', 'main')])])
def test_match(mocker, command, packages):
mocker.patch('thefuck.rules.apt_get.which', return_value=None)
mock = mocker.patch('thefuck.rules.apt_get.command_not_found',
create=True)
mock.getPackages.return_value = packages
mocker.patch('thefuck.rules.apt_get._get_packages',
create=True, return_value=packages)
assert match(command)
@ -30,9 +29,8 @@ def test_match(mocker, command, packages):
['vim'], '/usr/bin/vim')])
def test_not_match(mocker, command, packages, which):
mocker.patch('thefuck.rules.apt_get.which', return_value=which)
mock = mocker.patch('thefuck.rules.apt_get.command_not_found',
create=True)
mock.getPackages.return_value = packages
mocker.patch('thefuck.rules.apt_get._get_packages',
create=True, return_value=packages)
assert not match(command)
@ -49,7 +47,7 @@ def test_not_match(mocker, command, packages, which):
[('imagemagick', 'main'),
('graphicsmagick-imagemagick-compat', 'universe')])])
def test_get_new_command(mocker, command, new_command, packages):
mock = mocker.patch('thefuck.rules.apt_get.command_not_found',
create=True)
mock.getPackages.return_value = packages
mocker.patch('thefuck.rules.apt_get._get_packages',
create=True, return_value=packages)
assert get_new_command(command) == new_command

View File

@ -156,6 +156,8 @@ def test_get_operations(set_help, app, help_text, operations):
apt_get_help, 'apt-get install vim'),
('apt saerch vim', invalid_operation('saerch'),
apt_help, 'apt search vim'),
('apt uninstall vim', invalid_operation('uninstall'),
apt_help, 'apt remove vim'),
])
def test_get_new_command(set_help, output, script, help_text, result):
set_help(help_text)

View File

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck.rules.apt_list_upgradable import get_new_command, match
from thefuck.types import Command
match_output = '''
full_english_output = '''
Hit:1 http://us.archive.ubuntu.com/ubuntu zesty InRelease
Hit:2 http://us.archive.ubuntu.com/ubuntu zesty-updates InRelease
Get:3 http://us.archive.ubuntu.com/ubuntu zesty-backports InRelease [89.2 kB]
@ -17,6 +19,11 @@ Reading state information... Done
8 packages can be upgraded. Run 'apt list --upgradable' to see them.
'''
match_output = [
full_english_output,
'Führen Sie »apt list --upgradable« aus, um sie anzuzeigen.' # German
]
no_match_output = '''
Hit:1 http://us.archive.ubuntu.com/ubuntu zesty InRelease
Get:2 http://us.archive.ubuntu.com/ubuntu zesty-updates InRelease [89.2 kB]
@ -48,8 +55,9 @@ All packages are up to date.
'''
def test_match():
assert match(Command('sudo apt update', match_output))
@pytest.mark.parametrize('output', match_output)
def test_match(output):
assert match(Command('sudo apt update', output))
@pytest.mark.parametrize('command', [
@ -67,9 +75,10 @@ def test_not_match(command):
assert not match(command)
def test_get_new_command():
new_command = get_new_command(Command('sudo apt update', match_output))
@pytest.mark.parametrize('output', match_output)
def test_get_new_command(output):
new_command = get_new_command(Command('sudo apt update', output))
assert new_command == 'sudo apt list --upgradable'
new_command = get_new_command(Command('apt update', match_output))
new_command = get_new_command(Command('apt update', output))
assert new_command == 'apt list --upgradable'

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
import pytest
from thefuck.rules.cd_correction import match
from thefuck.types import Command
@pytest.mark.parametrize('command', [
Command('cd foo', 'cd: foo: No such file or directory'),
Command('cd foo/bar/baz',
'cd: foo: No such file or directory'),
Command('cd foo/bar/baz', 'cd: can\'t cd to foo/bar/baz'),
Command('cd /foo/bar/', 'cd: The directory "/foo/bar/" does not exist')])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('cd foo', ''), Command('', '')])
def test_not_match(command):
assert not match(command)
# Note that get_new_command uses local filesystem, so not testing it here.
# Instead, see the functional test `functional.test_cd_correction`

View File

@ -7,7 +7,8 @@ from thefuck.types import Command
Command('cd foo', 'cd: foo: No such file or directory'),
Command('cd foo/bar/baz',
'cd: foo: No such file or directory'),
Command('cd foo/bar/baz', 'cd: can\'t cd to foo/bar/baz')])
Command('cd foo/bar/baz', 'cd: can\'t cd to foo/bar/baz'),
Command('cd /foo/bar/', 'cd: The directory "/foo/bar/" does not exist')])
def test_match(command):
assert match(command)

View File

@ -0,0 +1,86 @@
import pytest
from thefuck.rules.choco_install import match, get_new_command
from thefuck.types import Command
package_not_found_error = (
'Chocolatey v0.10.15\n'
'Installing the following packages:\n'
'logstitcher\n'
'By installing you accept licenses for the packages.\n'
'logstitcher not installed. The package was not found with the source(s) listed.\n'
' Source(s): \'https://chocolatey.org/api/v2/\'\n'
' NOTE: When you specify explicit sources, it overrides default sources.\n'
'If the package version is a prerelease and you didn\'t specify `--pre`,\n'
' the package may not be found.\n'
'Please see https://chocolatey.org/docs/troubleshooting for more\n'
' assistance.\n'
'\n'
'Chocolatey installed 0/1 packages. 1 packages failed.\n'
' See the log for details (C:\\ProgramData\\chocolatey\\logs\\chocolatey.log).\n'
'\n'
'Failures\n'
' - logstitcher - logstitcher not installed. The package was not found with the source(s) listed.\n'
' Source(s): \'https://chocolatey.org/api/v2/\'\n'
' NOTE: When you specify explicit sources, it overrides default sources.\n'
'If the package version is a prerelease and you didn\'t specify `--pre`,\n'
' the package may not be found.\n'
'Please see https://chocolatey.org/docs/troubleshooting for more\n'
' assistance.\n'
)
@pytest.mark.parametrize('command', [
Command('choco install logstitcher', package_not_found_error),
Command('cinst logstitcher', package_not_found_error),
Command('choco install logstitcher -y', package_not_found_error),
Command('cinst logstitcher -y', package_not_found_error),
Command('choco install logstitcher -y -n=test', package_not_found_error),
Command('cinst logstitcher -y -n=test', package_not_found_error),
Command('choco install logstitcher -y -n=test /env', package_not_found_error),
Command('cinst logstitcher -y -n=test /env', package_not_found_error),
Command('choco install chocolatey -y', package_not_found_error),
Command('cinst chocolatey -y', package_not_found_error)])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('choco /?', ''),
Command('choco upgrade logstitcher', ''),
Command('cup logstitcher', ''),
Command('choco upgrade logstitcher -y', ''),
Command('cup logstitcher -y', ''),
Command('choco upgrade logstitcher -y -n=test', ''),
Command('cup logstitcher -y -n=test', ''),
Command('choco upgrade logstitcher -y -n=test /env', ''),
Command('cup logstitcher -y -n=test /env', ''),
Command('choco upgrade chocolatey -y', ''),
Command('cup chocolatey -y', ''),
Command('choco uninstall logstitcher', ''),
Command('cuninst logstitcher', ''),
Command('choco uninstall logstitcher -y', ''),
Command('cuninst logstitcher -y', ''),
Command('choco uninstall logstitcher -y -n=test', ''),
Command('cuninst logstitcher -y -n=test', ''),
Command('choco uninstall logstitcher -y -n=test /env', ''),
Command('cuninst logstitcher -y -n=test /env', ''),
Command('choco uninstall chocolatey -y', ''),
Command('cuninst chocolatey -y', '')])
def not_test_match(command):
assert not match(command)
@pytest.mark.parametrize('before, after', [
('choco install logstitcher', 'choco install logstitcher.install'),
('cinst logstitcher', 'cinst logstitcher.install'),
('choco install logstitcher -y', 'choco install logstitcher.install -y'),
('cinst logstitcher -y', 'cinst logstitcher.install -y'),
('choco install logstitcher -y -n=test', 'choco install logstitcher.install -y -n=test'),
('cinst logstitcher -y -n=test', 'cinst logstitcher.install -y -n=test'),
('choco install logstitcher -y -n=test /env', 'choco install logstitcher.install -y -n=test /env'),
('cinst logstitcher -y -n=test /env', 'cinst logstitcher.install -y -n=test /env'),
('choco install chocolatey -y', 'choco install chocolatey.install -y'),
('cinst chocolatey -y', 'cinst chocolatey.install -y'), ])
def test_get_new_command(before, after):
assert (get_new_command(Command(before, '')) == after)

View File

@ -0,0 +1,27 @@
from thefuck.rules.docker_image_being_used_by_container import match, get_new_command
from thefuck.types import Command
def test_match():
err_response = """Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1"""
assert match(Command('docker image rm -f cd809b04b6ff', err_response))
def test_not_match():
err_response = 'bash: docker: command not found'
assert not match(Command('docker image rm -f cd809b04b6ff', err_response))
def test_not_docker_command():
err_response = """Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1"""
assert not match(Command('git image rm -f cd809b04b6ff', err_response))
def test_get_new_command():
err_response = """
Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image
is being used by running container e5e2591040d1
"""
result = get_new_command(Command('docker image rm -f cd809b04b6ff', err_response))
expected = 'docker container rm -f e5e2591040d1 && docker image rm -f cd809b04b6ff'
assert result == expected

View File

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

View File

@ -4,6 +4,46 @@ from thefuck.types import Command
from thefuck.rules.docker_not_command import get_new_command, match
_DOCKER_SWARM_OUTPUT = '''
Usage: docker swarm COMMAND
Manage Swarm
Commands:
ca Display and rotate the root CA
init Initialize a swarm
join Join a swarm as a node and/or manager
join-token Manage join tokens
leave Leave the swarm
unlock Unlock swarm
unlock-key Manage the unlock key
update Update the swarm
Run 'docker swarm COMMAND --help' for more information on a command.
'''
_DOCKER_IMAGE_OUTPUT = '''
Usage: docker image COMMAND
Manage images
Commands:
build Build an image from a Dockerfile
history Show the history of an image
import Import the contents from a tarball to create a filesystem image
inspect Display detailed information on one or more images
load Load an image from a tar archive or STDIN
ls List images
prune Remove unused images
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rm Remove one or more images
save Save one or more images to a tar archive (streamed to STDOUT by default)
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
Run 'docker image COMMAND --help' for more information on a command.
'''
@pytest.fixture
def docker_help(mocker):
help = b'''Usage: docker [OPTIONS] COMMAND [arg...]
@ -104,6 +144,94 @@ Run 'docker COMMAND --help' for more information on a command.
return mock
@pytest.fixture
def docker_help_new(mocker):
helptext_new = b'''
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
Options:
--config string Location of client config files (default "/Users/ik1ne/.docker")
-c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var
and default context set with "docker context use")
-D, --debug Enable debug mode
-H, --host list Daemon socket(s) to connect to
-l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
--tls Use TLS; implied by --tlsverify
--tlscacert string Trust certs signed only by this CA (default "/Users/ik1ne/.docker/ca.pem")
--tlscert string Path to TLS certificate file (default "/Users/ik1ne/.docker/cert.pem")
--tlskey string Path to TLS key file (default "/Users/ik1ne/.docker/key.pem")
--tlsverify Use TLS and verify the remote
-v, --version Print version information and quit
Management Commands:
builder Manage builds
config Manage Docker configs
container Manage containers
context Manage contexts
image Manage images
network Manage networks
node Manage Swarm nodes
plugin Manage plugins
secret Manage Docker secrets
service Manage services
stack Manage Docker stacks
swarm Manage Swarm
system Manage Docker
trust Manage trust on Docker images
volume Manage volumes
Commands:
attach Attach local standard input, output, and error streams to a running container
build Build an image from a Dockerfile
commit Create a new image from a container's changes
cp Copy files/folders between a container and the local filesystem
create Create a new container
diff Inspect changes to files or directories on a container's filesystem
events Get real time events from the server
exec Run a command in a running container
export Export a container's filesystem as a tar archive
history Show the history of an image
images List images
import Import the contents from a tarball to create a filesystem image
info Display system-wide information
inspect Return low-level information on Docker objects
kill Kill one or more running containers
load Load an image from a tar archive or STDIN
login Log in to a Docker registry
logout Log out from a Docker registry
logs Fetch the logs of a container
pause Pause all processes within one or more containers
port List port mappings or a specific mapping for the container
ps List containers
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rename Rename a container
restart Restart one or more containers
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save one or more images to a tar archive (streamed to STDOUT by default)
search Search the Docker Hub for images
start Start one or more stopped containers
stats Display a live stream of container(s) resource usage statistics
stop Stop one or more running containers
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
top Display the running processes of a container
unpause Unpause all processes within one or more containers
update Update configuration of one or more containers
version Show the Docker version information
wait Block until one or more containers stop, then print their exit codes
Run 'docker COMMAND --help' for more information on a command.
'''
mock = mocker.patch('subprocess.Popen')
mock.return_value.stdout = BytesIO(b'')
mock.return_value.stderr = BytesIO(helptext_new)
return mock
def output(cmd):
return "docker: '{}' is not a docker command.\n" \
"See 'docker --help'.".format(cmd)
@ -113,6 +241,24 @@ def test_match():
assert match(Command('docker pes', output('pes')))
# tests docker (management command)
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, output', [
('docker swarn', output('swarn')),
('docker imge', output('imge'))])
def test_match_management_cmd(script, output):
assert match(Command(script, output))
# tests docker (management cmd) (management subcmd)
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, output', [
('docker swarm int', _DOCKER_SWARM_OUTPUT),
('docker image la', _DOCKER_IMAGE_OUTPUT)])
def test_match_management_subcmd(script, output):
assert match(Command(script, output))
@pytest.mark.parametrize('script, output', [
('docker ps', ''),
('cat pes', output('pes'))])
@ -120,10 +266,28 @@ def test_not_match(script, output):
assert not match(Command(script, output))
@pytest.mark.usefixtures('docker_help')
@pytest.mark.usefixtures('no_memoize', 'docker_help')
@pytest.mark.parametrize('wrong, fixed', [
('pes', ['ps', 'push', 'pause']),
('tags', ['tag', 'stats', 'images'])])
def test_get_new_command(wrong, fixed):
command = Command('docker {}'.format(wrong), output(wrong))
assert get_new_command(command) == ['docker {}'.format(x) for x in fixed]
@pytest.mark.usefixtures('no_memoize', 'docker_help_new')
@pytest.mark.parametrize('wrong, fixed', [
('swarn', ['swarm', 'start', 'search']),
('inage', ['image', 'images', 'rename'])])
def test_get_new_management_command(wrong, fixed):
command = Command('docker {}'.format(wrong), output(wrong))
assert get_new_command(command) == ['docker {}'.format(x) for x in fixed]
@pytest.mark.usefixtures('no_memoize', 'docker_help_new')
@pytest.mark.parametrize('wrong, fixed, output', [
('swarm int', ['swarm init', 'swarm join', 'swarm join-token'], _DOCKER_SWARM_OUTPUT),
('image la', ['image load', 'image ls', 'image tag'], _DOCKER_IMAGE_OUTPUT)])
def test_get_new_management_command_subcommand(wrong, fixed, output):
command = Command('docker {}'.format(wrong), output)
assert get_new_command(command) == ['docker {}'.format(x) for x in fixed]

View File

@ -0,0 +1,29 @@
import pytest
from thefuck.rules.git_branch_delete_checked_out import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def output():
return "error: Cannot delete branch 'foo' checked out at '/bar/foo'"
@pytest.mark.parametrize("script", ["git branch -d foo", "git branch -D foo"])
def test_match(script, output):
assert match(Command(script, output))
@pytest.mark.parametrize("script", ["git branch -d foo", "git branch -D foo"])
def test_not_match(script):
assert not match(Command(script, "Deleted branch foo (was a1b2c3d)."))
@pytest.mark.parametrize(
"script, new_command",
[
("git branch -d foo", "git checkout master && git branch -D foo"),
("git branch -D foo", "git checkout master && git branch -D foo"),
],
)
def test_get_new_command(script, new_command, output):
assert get_new_command(Command(script, output)) == new_command

View File

@ -4,7 +4,6 @@ from thefuck.rules.git_checkout import match, get_branches, get_new_command
from thefuck.types import Command
@pytest.fixture
def did_not_match(target, did_you_forget=False):
error = ("error: pathspec '{}' did not match any "
"file(s) known to git.".format(target))
@ -40,6 +39,11 @@ def test_not_match(command):
(b'', []),
(b'* master', ['master']),
(b' remotes/origin/master', ['master']),
(b' remotes/origin/test/1', ['test/1']),
(b' remotes/origin/test/1/2/3', ['test/1/2/3']),
(b' test/1', ['test/1']),
(b' test/1/2/3', ['test/1/2/3']),
(b' remotes/origin/HEAD -> origin/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',

View File

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

View File

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

View File

@ -0,0 +1,23 @@
import pytest
from thefuck.rules.git_merge_unrelated import match, get_new_command
from thefuck.types import Command
output = 'fatal: refusing to merge unrelated histories'
def test_match():
assert match(Command('git merge test', output))
assert not match(Command('git merge master', ''))
assert not match(Command('ls', output))
@pytest.mark.parametrize('command, new_command', [
(Command('git merge local', output),
'git merge local --allow-unrelated-histories'),
(Command('git merge -m "test" local', output),
'git merge -m "test" local --allow-unrelated-histories'),
(Command('git merge -m "test local" local', output),
'git merge -m "test local" local --allow-unrelated-histories')])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@ -15,6 +15,19 @@ To push the current branch and set the remote as upstream, use
'''.format(branch_name, branch_name)
@pytest.fixture
def output_bitbucket():
return '''Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: Create pull request for feature/set-upstream:
remote: https://bitbucket.org/set-upstream
remote:
To git@bitbucket.org:test.git
e5e7fbb..700d998 feature/set-upstream -> feature/set-upstream
Branch feature/set-upstream set up to track remote branch feature/set-upstream from origin.
'''
@pytest.mark.parametrize('script, branch_name', [
('git push', 'master'),
('git push origin', 'master')])
@ -22,6 +35,10 @@ def test_match(output, script, branch_name):
assert match(Command(script, output))
def test_match_bitbucket(output_bitbucket):
assert not match(Command('git push origin', output_bitbucket))
@pytest.mark.parametrize('script, branch_name', [
('git push master', None),
('ls', 'master')])
@ -49,6 +66,10 @@ def test_not_match(output, script, branch_name):
('git -c test=test push --quiet origin', 'master',
'git -c test=test push --set-upstream origin master --quiet'),
('git push', "test's",
"git push --set-upstream origin test\\'s")])
"git push --set-upstream origin test\\'s"),
('git push --force', 'master',
'git push --set-upstream origin master --force'),
('git push --force-with-lease', 'master',
'git push --set-upstream origin master --force-with-lease')])
def test_get_new_command(output, script, branch_name, new_command):
assert get_new_command(Command(script, output)) == new_command

View File

@ -0,0 +1,21 @@
import pytest
from thefuck.rules.go_unknown_command import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def build_misspelled_output():
return '''go bulid: unknown command
Run 'go help' for usage.'''
def test_match(build_misspelled_output):
assert match(Command('go bulid', build_misspelled_output))
def test_not_match():
assert not match(Command('go run', 'go run: no go files listed'))
def test_get_new_command(build_misspelled_output):
assert get_new_command(Command('go bulid', build_misspelled_output)) == 'go build'

View File

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

View File

@ -0,0 +1,25 @@
import pytest
from thefuck.rules.nixos_cmd_not_found import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize('command', [
Command('vim', 'nix-env -iA nixos.vim')])
def test_match(mocker, command):
mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None)
assert match(command)
@pytest.mark.parametrize('command', [
Command('vim', ''),
Command('', '')])
def test_not_match(mocker, command):
mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None)
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('vim', 'nix-env -iA nixos.vim'), 'nix-env -iA nixos.vim && vim'),
(Command('pacman', 'nix-env -iA nixos.pacman'), 'nix-env -iA nixos.pacman && pacman')])
def test_get_new_command(mocker, command, new_command):
assert get_new_command(command) == new_command

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
import pytest
from thefuck.rules.remove_shell_prompt_literal import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def output():
return "$: command not found"
@pytest.mark.parametrize("script", ["$ cd newdir", " $ cd newdir"])
def test_match(script, output):
assert match(Command(script, output))
@pytest.mark.parametrize(
"command",
[
Command("$", "$: command not found"),
Command(" $", "$: command not found"),
Command("$?", "127: command not found"),
Command(" $?", "127: command not found"),
Command("", ""),
],
)
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize(
"script, new_command",
[
("$ cd newdir", "cd newdir"),
("$ python3 -m virtualenv env", "python3 -m virtualenv env"),
],
)
def test_get_new_command(script, new_command, output):
assert get_new_command(Command(script, output)) == new_command

View File

@ -1,13 +1,17 @@
# -*- encoding: utf-8 -*-
import pytest
from thefuck.rules import switch_lang
from thefuck.types import Command
@pytest.mark.parametrize('command', [
Command(u'фзе-пуе', 'command not found: фзе-пуе'),
Command(u'λσ', 'command not found: λσ')])
Command(u'λσ', 'command not found: λσ'),
Command(u'שפא-עקא', 'command not found: שפא-עקא'),
Command(u'ךד', 'command not found: ךד'),
Command(u'녀애 ㅣㄴ', 'command not found: 녀애 ㅣㄴ')])
def test_match(command):
assert switch_lang.match(command)
@ -16,13 +20,20 @@ def test_match(command):
Command(u'pat-get', 'command not found: pat-get'),
Command(u'ls', 'command not found: ls'),
Command(u'агсл', 'command not found: агсл'),
Command(u'фзе-пуе', 'some info')])
Command(u'фзе-пуе', 'some info'),
Command(u'שפא-עקא', 'some info'),
Command(u'녀애 ㅣㄴ', 'some info')])
def test_not_match(command):
assert not switch_lang.match(command)
@pytest.mark.parametrize('command, new_command', [
(Command(u'фзе-пуе штыефдд мшь', ''), 'apt-get install vim'),
(Command(u'λσ -λα', ''), 'ls -la')])
(Command(u'λσ -λα', ''), 'ls -la'),
(Command(u'שפא-עקא ןמדאשךך הןצ', ''), 'apt-get install vim'),
(Command(u'ךד -ךש', ''), 'ls -la'),
(Command(u'멧-ㅎㄷㅅ ㅑㅜㄴㅅ미ㅣ 퍄ㅡ', ''), 'apt-get install vim'),
(Command(u'ㅣㄴ -ㅣㅁ', ''), 'ls -la'),
(Command(u'ㅔㅁㅅ촤', ''), 'patchk'), ])
def test_get_new_command(command, new_command):
assert switch_lang.get_new_command(command) == new_command

View File

@ -0,0 +1,33 @@
import pytest
from thefuck.rules.terraform_init import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize('script, output', [
('terraform plan', 'Error: Initialization required. '
'Please see the error message above.'),
('terraform plan', 'This module is not yet installed. Run "terraform init" '
'to install all modules required by this configuration.'),
('terraform apply', 'Error: Initialization required. '
'Please see the error message above.'),
('terraform apply', 'This module is not yet installed. Run "terraform init" '
'to install all modules required by this configuration.')])
def test_match(script, output):
assert match(Command(script, output))
@pytest.mark.parametrize('script, output', [
('terraform --version', 'Terraform v0.12.2'),
('terraform plan', 'No changes. Infrastructure is up-to-date.'),
('terraform apply', 'Apply complete! Resources: 0 added, 0 changed, 0 destroyed.'),
])
def test_not_match(script, output):
assert not match(Command(script, output=output))
@pytest.mark.parametrize('command, new_command', [
(Command('terraform plan', ''), 'terraform init && terraform plan'),
(Command('terraform apply', ''), 'terraform init && terraform apply'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

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

View File

@ -0,0 +1,22 @@
import pytest
from thefuck.rules.unsudo import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize('output', [
'you cannot perform this operation as root'])
def test_match(output):
assert match(Command('sudo ls', output))
def test_not_match():
assert not match(Command('', ''))
assert not match(Command('sudo ls', 'Permission denied'))
assert not match(Command('ls', 'you cannot perform this operation as root'))
@pytest.mark.parametrize('before, after', [
('sudo ls', 'ls'),
('sudo pacaur -S helloworld', 'pacaur -S helloworld')])
def test_get_new_command(before, after):
assert get_new_command(Command(before, '')) == after

View File

@ -0,0 +1,173 @@
from io import BytesIO
import pytest
from thefuck.rules.yum_invalid_operation import match, get_new_command, _get_operations
from thefuck.types import Command
yum_help_text = '''Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
Usage: yum [options] COMMAND
List of Commands:
check Check for problems in the rpmdb
check-update Check for available package updates
clean Remove cached data
deplist List a package's dependencies
distribution-synchronization Synchronize installed packages to the latest available versions
downgrade downgrade a package
erase Remove a package or packages from your system
fs Acts on the filesystem data of the host, mainly for removing docs/lanuages for minimal hosts.
fssnapshot Creates filesystem snapshots, or lists/deletes current snapshots.
groups Display, or use, the groups information
help Display a helpful usage message
history Display, or use, the transaction history
info Display details about a package or group of packages
install Install a package or packages on your system
langavailable Check available languages
langinfo List languages information
langinstall Install appropriate language packs for a language
langlist List installed languages
langremove Remove installed language packs for a language
list List a package or groups of packages
load-transaction load a saved transaction from filename
makecache Generate the metadata cache
provides Find what package provides the given value
reinstall reinstall a package
repo-pkgs Treat a repo. as a group of packages, so we can install/remove all of them
repolist Display the configured software repositories
search Search package details for the given string
shell Run an interactive yum shell
swap Simple way to swap packages, instead of using shell
update Update a package or packages on your system
update-minimal Works like upgrade, but goes to the 'newest' package match which fixes a problem that affects your system
updateinfo Acts on repository update information
upgrade Update packages taking obsoletes into account
version Display a version for the machine and/or available repos.
Options:
-h, --help show this help message and exit
-t, --tolerant be tolerant of errors
-C, --cacheonly run entirely from system cache, don't update cache
-c [config file], --config=[config file]
config file location
-R [minutes], --randomwait=[minutes]
maximum command wait time
-d [debug level], --debuglevel=[debug level]
debugging output level
--showduplicates show duplicates, in repos, in list/search commands
-e [error level], --errorlevel=[error level]
error output level
--rpmverbosity=[debug level name]
debugging output level for rpm
-q, --quiet quiet operation
-v, --verbose verbose operation
-y, --assumeyes answer yes for all questions
--assumeno answer no for all questions
--version show Yum version and exit
--installroot=[path] set install root
--enablerepo=[repo] enable one or more repositories (wildcards allowed)
--disablerepo=[repo] disable one or more repositories (wildcards allowed)
-x [package], --exclude=[package]
exclude package(s) by name or glob
--disableexcludes=[repo]
disable exclude from main, for a repo or for
everything
--disableincludes=[repo]
disable includepkgs for a repo or for everything
--obsoletes enable obsoletes processing during updates
--noplugins disable Yum plugins
--nogpgcheck disable gpg signature checking
--disableplugin=[plugin]
disable plugins by name
--enableplugin=[plugin]
enable plugins by name
--skip-broken skip packages with depsolving problems
--color=COLOR control whether color is used
--releasever=RELEASEVER
set value of $releasever in yum config and repo files
--downloadonly don't update, just download
--downloaddir=DLDIR specifies an alternate directory to store packages
--setopt=SETOPTS set arbitrary config and repo options
--bugfix Include bugfix relevant packages, in updates
--security Include security relevant packages, in updates
--advisory=ADVS, --advisories=ADVS
Include packages needed to fix the given advisory, in
updates
--bzs=BZS Include packages needed to fix the given BZ, in
updates
--cves=CVES Include packages needed to fix the given CVE, in
updates
--sec-severity=SEVS, --secseverity=SEVS
Include security relevant packages matching the
severity, in updates
Plugin Options:
--samearch-priorities
Priority-exclude packages based on name + arch
'''
yum_unsuccessful_search_text = '''Warning: No matches found for: {}
No matches found
'''
yum_successful_vim_search_text = '''================================================== N/S matched: vim ===================================================
protobuf-vim.x86_64 : Vim syntax highlighting for Google Protocol Buffers descriptions
vim-X11.x86_64 : The VIM version of the vi editor for the X Window System - GVim
vim-common.x86_64 : The common files needed by any version of the VIM editor
vim-enhanced.x86_64 : A version of the VIM editor which includes recent enhancements
vim-filesystem.x86_64 : VIM filesystem layout
vim-filesystem.noarch : VIM filesystem layout
vim-minimal.x86_64 : A minimal version of the VIM editor
Name and summary matches only, use "search all" for everything.
'''
yum_invalid_op_text = '''Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
No such command: {}. Please use /usr/bin/yum --help
'''
yum_operations = [
'check', 'check-update', 'clean', 'deplist', 'distribution-synchronization', 'downgrade', 'erase', 'fs',
'fssnapshot', 'groups', 'help', 'history', 'info', 'install', 'langavailable', 'langinfo', 'langinstall',
'langlist', 'langremove', 'list', 'load-transaction', 'makecache', 'provides', 'reinstall', 'repo-pkgs', 'repolist',
'search', 'shell', 'swap', 'update', 'update-minimal', 'updateinfo', 'upgrade', 'version', ]
@pytest.mark.parametrize('command', [
'saerch', 'uninstall',
])
def test_match(command):
assert match(Command('yum {}'.format(command), yum_invalid_op_text.format(command)))
@pytest.mark.parametrize('command, output', [
('vim', ''),
('yum', yum_help_text,),
('yum help', yum_help_text,),
('yum search asdf', yum_unsuccessful_search_text.format('asdf'),),
('yum search vim', yum_successful_vim_search_text)
])
def test_not_match(command, output):
assert not match(Command(command, output))
@pytest.fixture
def yum_help(mocker):
mock = mocker.patch('subprocess.Popen')
mock.return_value.stdout = BytesIO(bytes(yum_help_text.encode('utf-8')))
return mock
@pytest.mark.usefixtures('no_memoize', 'yum_help')
def test_get_operations():
assert _get_operations() == yum_operations
@pytest.mark.usefixtures('no_memoize', 'yum_help')
@pytest.mark.parametrize('script, output, result', [
('yum uninstall', yum_invalid_op_text.format('uninstall'), 'yum remove',),
('yum saerch asdf', yum_invalid_op_text.format('saerch'), 'yum search asdf',),
('yum hlep', yum_invalid_op_text.format('hlep'), 'yum help',),
])
def test_get_new_command(script, output, result):
assert get_new_command(Command(script, output))[0] == result

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,8 @@ class TestSettingsFromEnv(object):
'THEFUCK_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
'THEFUCK_WAIT_SLOW_COMMAND': '999',
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew'})
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew',
'THEFUCK_NUM_CLOSE_MATCHES': '359'})
settings.init()
assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim']
@ -63,6 +64,7 @@ class TestSettingsFromEnv(object):
assert settings.priority == {'bash': 10, 'vim': 15}
assert settings.wait_slow_command == 999
assert settings.slow_commands == ['lein', 'react-native', './gradlew']
assert settings.num_close_matches == 359
def test_from_env_with_DEFAULT(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@ -2,11 +2,11 @@
import pytest
import warnings
from mock import Mock
from mock import Mock, call, patch
from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, \
get_valid_history_without_current, _cache
get_valid_history_without_current, _cache, get_close_matches
from thefuck.types import Command
@ -50,6 +50,18 @@ class TestGetClosest(object):
fallback_to_first=False) is None
class TestGetCloseMatches(object):
@patch('thefuck.utils.difflib_get_close_matches')
def test_call_with_n(self, difflib_mock):
get_close_matches('', [], 1)
assert difflib_mock.call_args[0][2] == 1
@patch('thefuck.utils.difflib_get_close_matches')
def test_call_without_n(self, difflib_mock, settings):
get_close_matches('', [])
assert difflib_mock.call_args[0][2] == settings.get('num_close_matches')
@pytest.fixture
def get_aliases(mocker):
mocker.patch('thefuck.shells.shell.get_aliases',
@ -64,6 +76,24 @@ def test_get_all_executables():
assert 'fuck' not in all_callables
@pytest.fixture
def os_environ_pathsep(monkeypatch, path, pathsep):
env = {'PATH': path}
monkeypatch.setattr('os.environ', env)
monkeypatch.setattr('os.pathsep', pathsep)
return env
@pytest.mark.usefixtures('no_memoize', 'os_environ_pathsep')
@pytest.mark.parametrize('path, pathsep', [
('/foo:/bar:/baz:/foo/bar', ':'),
(r'C:\\foo;C:\\bar;C:\\baz;C:\\foo\\bar', ';')])
def test_get_all_executables_pathsep(path, pathsep):
with patch('thefuck.utils.Path') as Path_mock:
get_all_executables()
Path_mock.assert_has_calls([call(p) for p in path.split(pathsep)], True)
@pytest.mark.parametrize('args, result', [
(('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'),
(('git brnch', 'brnch', 'branch'), 'git branch')])
@ -195,9 +225,12 @@ 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', u'café ô'])
mock = mocker.patch('thefuck.shells.shell.get_history')
# Passing as an argument causes `UnicodeDecodeError`
# with newer py.test and python 2.7
mock.return_value = ['le cat', 'fuck', 'ls cat',
'diff x', 'nocommand x', u'café ô']
return mock
@pytest.fixture(autouse=True)
def alias(self, mocker):

View File

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

View File

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

View File

@ -42,6 +42,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'./gradlew', 'vagrant'],
'repeat': False,
'instant_mode': False,
'num_close_matches': 3,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@ -56,7 +57,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode'}
'THEFUCK_INSTANT_MODE': 'instant_mode',
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches'}
SETTINGS_HEADER = u"""# The Fuck settings file
#
@ -76,8 +78,12 @@ CONFIGURATION_TIMEOUT = 60
USER_COMMAND_MARK = u'\u200B' * 10
LOG_SIZE = 1000
LOG_SIZE_IN_BYTES = 1024 * 1024
LOG_SIZE_TO_CLEAN = 10 * 1024
DIFF_WITH_ALIAS = 0.5
SHELL_LOGGER_SOCKET_ENV = 'SHELL_LOGGER_SOCKET'
SHELL_LOGGER_LIMIT = 5

View File

@ -8,6 +8,7 @@ import sys # noqa: E402
from .. import logs # noqa: E402
from ..argument_parser import Parser # noqa: E402
from ..utils import get_installation_info # noqa: E402
from ..shells import shell # noqa: E402
from .alias import print_alias # noqa: E402
from .fix_command import fix_command # noqa: E402
@ -20,11 +21,14 @@ def main():
parser.print_help()
elif known_args.version:
logs.version(get_installation_info().version,
sys.version.split()[0])
elif known_args.command or 'TF_HISTORY' in os.environ:
fix_command(known_args)
sys.version.split()[0], shell.info())
# It's important to check if an alias is being requested before checking if
# `TF_HISTORY` is in `os.environ`, otherwise it might mess with subshells.
# Check https://github.com/nvbn/thefuck/issues/921 for reference
elif known_args.alias:
print_alias(known_args)
elif known_args.command or 'TF_HISTORY' in os.environ:
fix_command(known_args)
elif known_args.shell_logger:
try:
from .shell_logger import shell_logger # noqa: E402

View File

@ -3,6 +3,7 @@ from ..system import init_output
init_output()
import getpass # noqa: E402
import os # noqa: E402
import json # noqa: E402
from tempfile import gettempdir # noqa: E402
@ -27,7 +28,9 @@ def _get_shell_pid():
def _get_not_configured_usage_tracker_path():
"""Returns path of special file where we store latest shell pid."""
return Path(gettempdir()).joinpath('thefuck.last_not_configured_run')
return Path(gettempdir()).joinpath(u'thefuck.last_not_configured_run_{}'.format(
getpass.getuser(),
))
def _record_first_run():

View File

@ -1,19 +1,26 @@
import array
import fcntl
from functools import partial
import mmap
import os
import pty
import signal
import sys
import termios
import tty
from ..logs import warn
from .. import logs, const
def _read(f, fd):
data = os.read(fd, 1024)
f.write(data)
f.flush()
try:
f.write(data)
except ValueError:
position = const.LOG_SIZE_IN_BYTES - const.LOG_SIZE_TO_CLEAN
f.move(0, const.LOG_SIZE_TO_CLEAN, position)
f.seek(position)
f.write(b'\x00' * const.LOG_SIZE_TO_CLEAN)
f.seek(position)
return data
@ -61,10 +68,12 @@ def shell_logger(output):
"""
if not os.environ.get('SHELL'):
warn("Shell logger doesn't support your platform.")
logs.warn("Shell logger doesn't support your platform.")
sys.exit(1)
with open(output, 'wb') as f:
return_code = _spawn(os.environ['SHELL'], partial(_read, f))
fd = os.open(output, os.O_CREAT | os.O_TRUNC | os.O_RDWR)
os.write(fd, b'\x00' * const.LOG_SIZE_IN_BYTES)
buffer = mmap.mmap(fd, const.LOG_SIZE_IN_BYTES, mmap.MAP_SHARED, mmap.PROT_WRITE)
return_code = _spawn(os.environ['SHELL'], partial(_read, buffer))
sys.exit(return_code)

View File

@ -106,7 +106,7 @@ def how_to_configure_alias(configuration_details):
if configuration_details.can_configure_automatically:
print(
u"Or run {bold}fuck{reset} second time for configuring"
u"Or run {bold}fuck{reset} a second time to configure"
u" it automatically.".format(
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL)))
@ -134,7 +134,8 @@ def configured_successfully(configuration_details):
reload=configuration_details.reload))
def version(thefuck_version, python_version):
def version(thefuck_version, python_version, shell_info):
sys.stderr.write(
u'The Fuck {} using Python {}\n'.format(thefuck_version,
python_version))
u'The Fuck {} using Python {} and {}\n'.format(thefuck_version,
python_version,
shell_info))

View File

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

View File

@ -1,5 +1,7 @@
import os
import shlex
import mmap
import re
try:
from shutil import get_terminal_size
except ImportError:
@ -18,11 +20,6 @@ def _group_by_calls(log):
script_line = None
lines = []
for line in log:
try:
line = line.decode()
except UnicodeDecodeError:
continue
if const.USER_COMMAND_MARK in line or ps1_counter > 0:
if script_line and ps1_counter == 0:
yield script_line, lines
@ -53,13 +50,14 @@ def _get_script_group_lines(grouped, script):
def _get_output_lines(script, log_file):
lines = log_file.readlines()[-const.LOG_SIZE:]
data = log_file.read().decode()
data = re.sub(r'\x00+$', '', data)
lines = data.split('\n')
grouped = list(_group_by_calls(lines))
script_lines = _get_script_group_lines(grouped, script)
screen = pyte.Screen(get_terminal_size().columns, len(script_lines))
stream = pyte.Stream(screen)
stream.feed(''.join(script_lines))
stream.feed('\n'.join(script_lines))
return screen.display
@ -91,10 +89,11 @@ def get_output(script):
return None
try:
with logs.debug_time(u'Read output from log'), \
open(os.environ['THEFUCK_OUTPUT_LOG'], 'rb') as log_file:
_skip_old_lines(log_file)
lines = _get_output_lines(script, log_file)
with logs.debug_time(u'Read output from log'):
fd = os.open(os.environ['THEFUCK_OUTPUT_LOG'], os.O_RDONLY)
buffer = mmap.mmap(fd, const.LOG_SIZE_IN_BYTES, mmap.MAP_SHARED, mmap.PROT_READ)
_skip_old_lines(buffer)
lines = _get_output_lines(script, buffer)
output = '\n'.join(lines).strip()
logs.debug(u'Received output: {}'.format(output))
return output

View File

@ -1,11 +1,25 @@
import os
import shlex
from subprocess import Popen, PIPE, STDOUT
from psutil import Process, TimeoutExpired
from psutil import AccessDenied, Process, TimeoutExpired
from .. import logs
from ..conf import settings
def _kill_process(proc):
"""Tries to kill the process otherwise just logs a debug message, the
process will be killed when thefuck terminates.
:type proc: Process
"""
try:
proc.kill()
except AccessDenied:
logs.debug(u'Rerun: process PID {} ({}) could not be terminated'.format(
proc.pid, proc.exe()))
def _wait_output(popen, is_slow):
"""Returns `True` if we can get output of the command in the
`settings.wait_command` time.
@ -23,8 +37,8 @@ def _wait_output(popen, is_slow):
return True
except TimeoutExpired:
for child in proc.children(recursive=True):
child.kill()
proc.kill()
_kill_process(child)
_kill_process(proc)
return False
@ -39,8 +53,9 @@ def get_output(script, expanded):
env = dict(os.environ)
env.update(settings.env)
is_slow = shlex.split(expanded) in settings.slow_commands
with logs.debug_time(u'Call: {}; with env: {}; is slow: '.format(
split_expand = shlex.split(expanded)
is_slow = split_expand[0] in settings.slow_commands if split_expand else False
with logs.debug_time(u'Call: {}; with env: {}; is slow: {}'.format(
script, env, is_slow)):
result = Popen(expanded, shell=True, stdin=PIPE,
stdout=PIPE, stderr=STDOUT, env=env)

View File

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

View File

@ -1,3 +1,4 @@
from types import ModuleType
from thefuck.specific.apt import apt_available
from thefuck.utils import memoize, which
from thefuck.shells import shell
@ -5,8 +6,14 @@ from thefuck.shells import shell
try:
from CommandNotFound import CommandNotFound
command_not_found = CommandNotFound()
enabled_by_default = apt_available
if isinstance(CommandNotFound, ModuleType):
# For ubuntu 18.04+
_get_packages = CommandNotFound.CommandNotFound().get_packages
else:
# For older versions
_get_packages = CommandNotFound().getPackages
except ImportError:
enabled_by_default = False
@ -21,7 +28,7 @@ def _get_executable(command):
@memoize
def get_package(executable):
try:
packages = command_not_found.getPackages(executable)
packages = _get_packages(executable)
return packages[0][0]
except IndexError:
# IndexError is thrown when no matching package is found

View File

@ -8,7 +8,7 @@ enabled_by_default = apt_available
@sudo_support
@for_app('apt')
def match(command):
return "Run 'apt list --upgradable' to see them." in command.output
return 'apt list --upgradable' in command.output
@sudo_support

View File

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

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

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

View File

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

View File

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

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

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

View File

@ -2,10 +2,9 @@
import os
import six
from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
from thefuck.utils import for_app, get_close_matches
__author__ = "mmussomele"
@ -21,9 +20,12 @@ def _get_sub_dirs(parent):
@for_app('cd')
def match(command):
"""Match function copied from cd_mkdir.py"""
return (command.script.startswith('cd ')
and ('no such file or directory' in command.output.lower()
or 'cd: can\'t cd to' in command.output.lower()))
return (
command.script.startswith('cd ') and any((
'no such file or directory' in command.output.lower(),
'cd: can\'t cd to' in command.output.lower(),
'does not exist' in command.output.lower()
)))
@sudo_support
@ -37,7 +39,11 @@ def get_new_command(command):
dest = command.script_parts[1].split(os.sep)
if dest[-1] == '':
dest = dest[:-1]
if six.PY2:
if dest[0] == '':
cwd = os.sep
dest = dest[1:]
elif six.PY2:
cwd = os.getcwdu()
else:
cwd = os.getcwd()

View File

@ -8,10 +8,11 @@ from thefuck.shells import shell
@for_app('cd')
def match(command):
return (
'no such file or directory' in command.output.lower()
or 'cd: can\'t cd to' in command.output.lower()
or 'the system cannot find the path specified.' in command.output.lower()
)
command.script.startswith('cd ') and any((
'no such file or directory' in command.output.lower(),
'cd: can\'t cd to' in command.output.lower(),
'does not exist' in command.output.lower()
)))
@sudo_support

View File

@ -0,0 +1,25 @@
from thefuck.utils import for_app, which
@for_app("choco", "cinst")
def match(command):
return ((command.script.startswith('choco install') or 'cinst' in command.script_parts)
and 'Installing the following packages' in command.output)
def get_new_command(command):
# Find the argument that is the package name
for script_part in command.script_parts:
if (
script_part not in ["choco", "cinst", "install"]
# Need exact match (bc chocolatey is a package)
and not script_part.startswith('-')
# Leading hyphens are parameters; some packages contain them though
and '=' not in script_part and '/' not in script_part
# These are certainly parameters
):
return command.script.replace(script_part, script_part + ".install")
return []
enabled_by_default = bool(which("choco")) or bool(which("cinst"))

View File

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

View File

@ -0,0 +1,20 @@
from thefuck.utils import for_app
from thefuck.shells import shell
@for_app('docker')
def match(command):
'''
Matches a command's output with docker's output
warning you that you need to remove a container before removing an image.
'''
return 'image is being used by running container' in command.output
def get_new_command(command):
'''
Prepends docker container rm -f {container ID} to
the previous docker image rm {image ID} command
'''
container_id = command.output.strip().split(' ')
return shell.and_('docker container rm -f {}', '{}').format(container_id[-1], command.script)

View File

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

View File

@ -8,16 +8,30 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
@for_app('docker')
def match(command):
return 'is not a docker command' in command.output
return 'is not a docker command' in command.output or 'Usage: docker' in command.output
def _parse_commands(lines, starts_with):
lines = dropwhile(lambda line: not line.startswith(starts_with), lines)
lines = islice(lines, 1, None)
lines = list(takewhile(lambda line: line.strip(), lines))
return [line.strip().split(' ')[0] for line in lines]
def get_docker_commands():
proc = subprocess.Popen('docker', stdout=subprocess.PIPE)
lines = [line.decode('utf-8') for line in proc.stdout.readlines()]
lines = dropwhile(lambda line: not line.startswith('Commands:'), lines)
lines = islice(lines, 1, None)
lines = list(takewhile(lambda line: line != '\n', lines))
return [line.strip().split(' ')[0] for line in lines]
proc = subprocess.Popen('docker', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Old version docker returns its output to stdout, while newer version returns to stderr.
lines = proc.stdout.readlines() or proc.stderr.readlines()
lines = [line.decode('utf-8') for line in lines]
# Only newer versions of docker have management commands in the help text.
if 'Management Commands:\n' in lines:
management_commands = _parse_commands(lines, 'Management Commands:')
else:
management_commands = []
regular_commands = _parse_commands(lines, 'Commands:')
return management_commands + regular_commands
if which('docker'):
@ -26,6 +40,10 @@ if which('docker'):
@sudo_support
def get_new_command(command):
if 'Usage:' in command.output and len(command.script_parts) > 1:
management_subcommands = _parse_commands(command.output.split('\n'), 'Commands:')
return replace_command(command, command.script_parts[2], management_subcommands)
wrong_command = re.findall(
r"docker: '(\w+)' is not a docker command.", command.output)[0]
return replace_command(command, wrong_command, get_docker_commands())

View File

@ -0,0 +1,19 @@
from thefuck.shells import shell
from thefuck.specific.git import git_support
from thefuck.utils import replace_argument
@git_support
def match(command):
return (
("branch -d" in command.script or "branch -D" in command.script)
and "error: Cannot delete branch '" in command.output
and "' checked out at '" in command.output
)
@git_support
def get_new_command(command):
return shell.and_("git checkout master", "{}").format(
replace_argument(command.script, "-d", "-D")
)

View File

@ -8,7 +8,7 @@ from thefuck.shells import shell
@git_support
def match(command):
return ('did not match any file(s) known to git.' in command.output
return ('did not match any file(s) known to git' in command.output
and "Did you forget to 'git add'?" not in command.output)
@ -18,10 +18,12 @@ def get_branches():
stdout=subprocess.PIPE)
for line in proc.stdout.readlines():
line = line.decode('utf-8')
if '->' in line: # Remote HEAD like b' remotes/origin/HEAD -> origin/master'
continue
if line.startswith('*'):
line = line.split(' ')[1]
if '/' in line:
line = line.split('/')[-1]
if line.strip().startswith('remotes/'):
line = '/'.join(line.split('/')[2:])
yield line.strip()
@ -29,7 +31,7 @@ def get_branches():
def get_new_command(command):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
r"did not match any file\(s\) known to git.", command.output)[0]
r"did not match any file\(s\) known to git", command.output)[0]
closest_branch = utils.get_closest(missing_file, get_branches(),
fallback_to_first=False)
if closest_branch:

View File

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

View File

@ -0,0 +1,12 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('merge' in command.script
and 'fatal: refusing to merge unrelated histories' in command.output)
@git_support
def get_new_command(command):
return command.script + ' --allow-unrelated-histories'

View File

@ -6,7 +6,7 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
return ('push' in command.script_parts
and 'set-upstream' in command.output)
and 'git push --set-upstream' in command.output)
def _get_upstream_option_index(command_parts):
@ -39,6 +39,6 @@ def get_new_command(command):
while len(command_parts) > push_idx and command_parts[len(command_parts) - 1][0] != '-':
command_parts.pop(len(command_parts) - 1)
arguments = re.findall(r'git push (.*)', command.output)[0].replace("'", r"\'").strip()
arguments = re.findall(r'git push (.*)', command.output)[-1].replace("'", r"\'").strip()
return replace_argument(" ".join(command_parts), 'push',
'push {}'.format(arguments))

View File

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

View File

@ -0,0 +1,28 @@
from itertools import dropwhile, islice, takewhile
import subprocess
from thefuck.utils import get_closest, replace_argument, for_app, which, cache
def get_golang_commands():
proc = subprocess.Popen('go', stderr=subprocess.PIPE)
lines = [line.decode('utf-8').strip() for line in proc.stderr.readlines()]
lines = dropwhile(lambda line: line != 'The commands are:', lines)
lines = islice(lines, 2, None)
lines = takewhile(lambda line: line, lines)
return [line.split(' ')[0] for line in lines]
if which('go'):
get_docker_commands = cache(which('go'))(get_golang_commands)
@for_app('go')
def match(command):
return 'unknown command' in command.output
def get_new_command(command):
closest_subcommand = get_closest(command.script_parts[1], get_golang_commands())
return replace_argument(command.script, command.script_parts[1],
closest_subcommand)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
import re
from thefuck.specific.nix import nix_available
from thefuck.shells import shell
regex = re.compile(r'nix-env -iA ([^\s]*)')
enabled_by_default = nix_available
def match(command):
return regex.findall(command.output)
def get_new_command(command):
name = regex.findall(command.output)[0]
return shell.and_('nix-env -iA {}'.format(name), command.script)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,22 @@
"""Fixes error for commands containing the shell prompt symbol '$'.
This usually happens when commands are copied from documentations
including them in their code blocks.
Example:
> $ git clone https://github.com/nvbn/thefuck.git
bash: $: command not found...
"""
import re
def match(command):
return (
"$: command not found" in command.output
and re.search(r"^[\s]*\$ [\S]+", command.script) is not None
)
def get_new_command(command):
return command.script.replace("$", "", 1).strip()

View File

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

View File

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

View File

@ -0,0 +1,13 @@
from thefuck.shells import shell
from thefuck.utils import for_app
@for_app('terraform')
def match(command):
return ('this module is not yet installed' in command.output.lower() or
'initialization required' in command.output.lower()
)
def get_new_command(command):
return shell.and_('terraform init', command.script)

View File

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

Some files were not shown because too many files have changed in this diff Show More