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

Compare commits

...

28 Commits
3.30 ... 3.31

Author SHA1 Message Date
Vladimir Iakovlev
0949d2e770 Bump to 3.31 2021-06-09 21:50:44 +02:00
Vladimir Iakovlev
e343c577cd NA: Fix possible changes in files outside of working directory (#1206) 2021-06-08 22:04:51 +02:00
Stuart Leeks
6da0bc557f Add excluded_search_path_prefixes setting (#1165)
Improves performance in WSL

Fix #1036

* Add excluded_search_path_prefixes setting

Allows filtering the paths used to search for commands
Can be useful to filter out /mnt/ in WSL for performance

* Add test for excluded_search_path_prefixes

* Apply suggestions from code review

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2021-04-21 19:43:21 +02:00
Pablo Santiago Blum de Aguiar
1a595f1ba2 #N/A: Force decorator<5 for Python <= 2.7
For some reason, the index used by GH Actions is not considering the
requesting Python version to decide which package version to deliver.
This is unnecessary for the official index and will be removed once we
drop support to Python 2.
2021-04-18 15:48:39 +02:00
godofhyperdeath
875d3f11cb #N/A: Update license year to 2020-2021 (#1088)
Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2021-04-17 00:24:30 +02:00
Sid Shardanand
4c7479b3ad #N/A: Add cd_cs rule (#1167)
* adding in files for the cd-cs feature

* Updated thefuck/rules/cd_cs.py comments to be more verbose

Thanks Scorphus!

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>

* Updating the rules file to exclude the \xe2 character

This character(–) has lead to the commit failing some of the tests. 
I am removing it from the code and we should see the tests pass now.

* Setting the encoding in thefuck/rules/cd_cs.py

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>

Co-authored-by: SID SHARDANAND <sshardan@deakin.edu.au>
Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2021-03-22 20:55:45 +01:00
Divy Jain
5b612add74 #1154: Fix badges in README (#1175) 2021-03-13 14:24:53 +01:00
Pablo Santiago Blum de Aguiar
b9dd54c768 #1174: Fix anchor references 2021-03-13 12:50:09 +01:00
Bhavishya Pandit
7af9f41d93 #N/A: Add a Contents section to README (#1174)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2021-03-13 00:58:10 +01:00
Pablo Santiago Blum de Aguiar
c2cc95db88 #1117: Mock Popen in go_unknown_command test
Instead of ignoring it whenever `go` is unavailable
2021-02-22 22:54:45 +01:00
Georgios Kontosis
0e34c2343e #/N/A: Extend pyenv rule to include goenv, nodenv and rbenv (#1100)
Co-authored-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
Co-authored-by: Divy Jain <dkj@somaiya.edu>
2021-02-11 12:48:20 +01:00
Connor Martin
fd90e69ceb #N/A: Add conda rule (#1138)
* add conda rules

* revert

* add conda

* add to readme and flake

* consistency with quotes and use for_app

* Update thefuck/rules/conda_mistype.py

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2021-02-08 13:04:59 +01:00
Divy Jain
0c58317932 #N/A: Migrate CI pipeline to Github Actions (#1154)
Co-authored-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2021-01-21 14:34:01 +01:00
Divy Jain
62dddd5821 #1149: Add python_module_error rule (#1151) 2021-01-19 22:37:05 +01:00
Divy Jain
40dd65963d #1141: Fix crash on empty history (#1145) 2020-11-18 10:43:11 +01:00
Kartik Soneji
836f6eeac5 Skip test instead of failing if go executable is not found. (#1117) 2020-11-03 18:30:03 +01:00
Kartik Soneji
b4c75eebe6 Fix pytest warnings (#1116)
* Add custom pytest mark.

* Fix typo usefixture -> usefixtures.
2020-11-03 18:29:28 +01:00
Pablo Aguiar
22efa8f70e #1113: Do not load excluded rules (#1125) 2020-11-03 18:27:09 +01:00
Pablo Aguiar
9d3bcad229 #1113: Ignore a rule that fails to load (#1124) 2020-11-03 18:26:13 +01:00
Divya Jain
c196e2901c #509: Fixed correction on windows machine running other shells (#1091)
* Replaced print with sys.stdout.write

* Fixed tests

* Normalized line endings
2020-07-16 23:56:58 +02:00
Aditi Gupta
ca46956e20 #1066 - Fix rule for brew cask (#1111)
* #1066: Add cask to list of fallback commands

* #1066: Fix homebrew paths
2020-07-16 23:34:41 +02:00
Nyanotech
639e9bda7a Add rule to remove a doubled-up "git clone" in a git clone command. (#1106)
Some git hosts will copy the entire clone command, while others just
copy the url, so typing "git clone ", then pasting a git url that
already has a "git clone " on the front is a somewhat common issue.
2020-07-16 23:34:22 +02:00
Divya Jain
39753a004e #1096: Rule: Bypass failed git hook (#1097)
* naive implementation

* better implementation

* remove redundant enabled_by_default

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>

* add README entry

* add tests

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2020-07-16 23:32:29 +02:00
Divya Jain
365db1ee41 #N/A: Refactor for better readability (#1094)
* #N/A: Refactor for better readability

* Change typename for test tuple

* Rename variable
2020-06-14 16:20:40 +02:00
Divya Jain
81b39defe4 #N/A: Fix formatting (#1092)
* Fixed corrector.py format string

* Fixed types.py format string

* Fixed tests/rules/test_fix_file.py formatting

* Removed trailing whitespace

* Fixed UnicodeEncodeError in python 2.7
2020-06-11 00:21:45 +02:00
Connor Martin
f82176802e add git-lfs support (#1056)
* add mistyping support for git lfs

* add the rule

* fix flake8

* flake8

* add to readme

* use fixtures and regex

* get rid of additional matched strings
2020-06-11 00:20:37 +02:00
Pablo Aguiar
6975d30818 #960: Improve pacman_invalid_option rule (#1072)
The rule now matches all possible lower case options and only those, not
failing for those that cannot be fixed.
2020-04-06 21:46:40 +02:00
DragonGhost7
3c542a5b8c Added pacman invalid option rule (#960)
* Added pacman invalid option rule

* Added test

* flake8 fixes

* Test changes

* Test fix

* Typo - again

* Travis test fix

* More Travis

* Even more travis

* I hope im right here

* Update README.md

Co-Authored-By: Pablo Aguiar <scorphus@gmail.com>

* Update thefuck/rules/pacman_invalid_option.py

Co-Authored-By: Pablo Aguiar <scorphus@gmail.com>

Co-authored-by: Pablo Aguiar <scorphus@gmail.com>
2020-03-28 23:15:20 +01:00
38 changed files with 666 additions and 196 deletions

49
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Tests
on: [push, pull_request]
env:
PYTHON_LATEST: 3.9
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Cache dependencies
id: cache-deps
uses: actions/cache@v2
with:
path: |
${{ env.pythonLocation }}/bin/*
${{ env.pythonLocation }}/lib/*
${{ env.pythonLocation }}/scripts/*
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'requirements.txt') }}
- name: Install The Fuck with all dependencies
if: steps.cache-deps.outputs.cache-hit != 'true'
run: |
pip install -Ur requirements.txt coveralls
python setup.py develop
- name: Lint
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
run: flake8
- name: Run tests
if: matrix.os != 'ubuntu-latest' || matrix.python-version != env.PYTHON_LATEST
run: coverage run --source=thefuck,tests -m pytest -v --capture=sys tests
- name: Run tests (including functional)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
run: coverage run --source=thefuck,tests -m pytest -v --capture=sys tests --enable-functional
- name: Post coverage results
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: coveralls --service=github

View File

@@ -1,51 +0,0 @@
language: python
sudo: false
os: linux
dist: xenial
matrix:
include:
- 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
language: generic
allow_failures:
- python: nightly
- python: 3.8-dev
- python: 3.7-dev
- python: 3.6-dev
services:
- docker
addons:
apt:
packages:
- python-commandnotfound
- python3-commandnotfound
before_install:
- 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 unlink python@2; 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
install:
- pip install -Ur requirements.txt
- python setup.py develop
- rm -rf build
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.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.8 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
=====================
Copyright (c) 2015-2018 Vladimir Iakovlev
Copyright (c) 2015-2021 Vladimir Iakovlev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,4 +1,4 @@
# 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)
# The Fuck [![Version][version-badge]][version-link] [![Build Status][workflow-badge]][workflow-link] [![Coverage][coverage-badge]][coverage-link] [![MIT License][license-badge]](LICENSE.md)
*The Fuck* is a magnificent app, inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320),
@@ -91,12 +91,27 @@ Reading package lists... Done
...
```
## Contents
1. [Requirements](#requirements)
2. [Installations](#installation)
3. [Updating](#updating)
4. [How it works](#how-it-works)
5. [Creating your own rules](#creating-your-own-rules)
6. [Settings](#settings)
7. [Third party packages with rules](#third-party-packages-with-rules)
8. [Experimental instant mode](#experimental-instant-mode)
9. [Developing](#developing)
10. [License](#license-mit)
## Requirements
- python (3.4+)
- pip
- python-dev
##### [Back to Contents](#contents)
## Installation
On OS X, you can install *The Fuck* via [Homebrew][homebrew] (or via [Linuxbrew][linuxbrew] on Linux):
@@ -157,6 +172,8 @@ To fix commands recursively until succeeding, use the `-r` option:
fuck -r
```
##### [Back to Contents](#contents)
## Updating
```bash
@@ -179,11 +196,13 @@ following rules are enabled by default:
* `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_cs` &ndash; changes `cs` to `cd`;
* `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;
* `conda_mistype` &ndash; fixes conda commands;
* `cp_create_destination` &ndash; creates a new directory when you attempt to `cp` or `mv` to a non existent one
* `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory;
* `cpp11` &ndash; adds missing `-std=c++11` to `g++` or `clang++`;
@@ -207,6 +226,7 @@ following rules are enabled by default:
* `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_clone_git_clone` &ndash; replaces `git clone git clone ...` with `git clone ...`
* `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;
@@ -214,6 +234,8 @@ following rules are enabled by default:
* `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_hook_bypass` &ndash; adds `--no-verify` flag previous to `git am`, `git commit`, or `git push` command;
* `git_lfs_mistype` &ndash; fixes mistyped `git lfs <command>` commands;
* `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`;
@@ -269,15 +291,16 @@ following rules are enabled by default:
* `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;
* `omnienv_no_such_command` &ndash; fixes wrong commands for `goenv`, `nodenv`, `pyenv` and `rbenv` (eg.: `pyenv isntall` or `goenv list`);
* `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;
* `pyenv_no_such_command` &ndash; fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`;
* `python_command` &ndash; prepends `python` when you try to run non-executable/without `./` python script;
* `python_execute` &ndash; appends missing `.py` when executing Python files;
* `python_module_error` &ndash; fixes ModuleNotFoundError by trying to `pip install` that module;
* `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;
@@ -308,6 +331,8 @@ following rules are enabled by default:
* `yarn_command_replaced` &ndash; fixes replaced `yarn` commands;
* `yarn_help` &ndash; makes it easier to open `yarn` documentation;
##### [Back to Contents](#contents)
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`);
@@ -325,6 +350,7 @@ The following rules are enabled by default on specific platforms only:
* `dnf_no_such_command` &ndash; fixes mistyped DNF commands;
* `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_invalid_option` &ndash; replaces lowercase `pacman` options with uppercase.
* `pacman_not_found` &ndash; fixes package name with `pacman`, `yay` or `yaourt`.
* `yum_invalid_operation` &ndash; fixes invalid `yum` calls, like `yum isntall vim`;
@@ -334,6 +360,8 @@ 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.
##### [Back to Contents](#contents)
## Creating your own rules
To add your own rule, create a file named `your-rule-name.py`
@@ -387,6 +415,8 @@ requires_output = True
[utility functions for rules](https://github.com/nvbn/thefuck/tree/master/thefuck/utils.py),
[app/os-specific helpers](https://github.com/nvbn/thefuck/tree/master/thefuck/specific/).
##### [Back to Contents](#contents)
## Settings
Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefuck/settings.py`
@@ -404,6 +434,7 @@ Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefu
* `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;
* `num_close_matches` &ndash; maximum number of close matches to suggest, by default `3`.
* `excluded_search_path_prefixes` &ndash; path prefixes to ignore when searching for commands, by default `[]`.
An example of `settings.py`:
@@ -436,6 +467,7 @@ rule with lower `priority` will be matched first;
* `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_NUM_CLOSE_MATCHES` &ndash; maximum number of close matches to suggest, like `5`.
* `THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES` &ndash; path prefixes to ignore when searching for commands, by default `[]`.
For example:
@@ -450,6 +482,8 @@ export THEFUCK_HISTORY_LIMIT='2000'
export THEFUCK_NUM_CLOSE_MATCHES='5'
```
##### [Back to Contents](#contents)
## Third-party packages with rules
If you'd like to make a specific set of non-public rules, but would still like
@@ -469,6 +503,8 @@ thefuck_contrib_foo
*The Fuck* will find rules located in the `rules` module.
##### [Back to Contents](#contents)
## Experimental instant mode
The default behavior of *The Fuck* requires time to re-run previous commands.
@@ -488,6 +524,8 @@ For example:
eval $(thefuck --alias --enable-experimental-instant-mode)
```
##### [Back to Contents](#contents)
## Developing
See [CONTRIBUTING.md](CONTRIBUTING.md)
@@ -498,10 +536,8 @@ Project License can be found [here](LICENSE.md).
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
[version-link]: https://pypi.python.org/pypi/thefuck/
[travis-badge]: https://travis-ci.org/nvbn/thefuck.svg?branch=master
[travis-link]: https://travis-ci.org/nvbn/thefuck
[appveyor-badge]: https://ci.appveyor.com/api/projects/status/1sskj4imj02um0gu/branch/master?svg=true
[appveyor-link]: https://ci.appveyor.com/project/nvbn/thefuck
[workflow-badge]: https://github.com/nvbn/thefuck/workflows/Tests/badge.svg
[workflow-link]: https://github.com/nvbn/thefuck/actions?query=workflow%3ATests
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
[coverage-link]: https://coveralls.io/github/nvbn/thefuck
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
@@ -509,3 +545,5 @@ Project License can be found [here](LICENSE.md).
[instant-mode-gif-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example_instant_mode.gif
[homebrew]: https://brew.sh/
[linuxbrew]: https://linuxbrew.sh/
##### [Back to Contents](#contents)

View File

@@ -1,23 +0,0 @@
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
- PYTHON: "C:/Python35"
- PYTHON: "C:/Python36"
- PYTHON: "C:/Python37"
init:
- "ECHO %PYTHON%"
- ps: "ls C:/Python*"
install:
- "curl -fsS -o C:/get-pip.py https://bootstrap.pypa.io/get-pip.py"
- "%PYTHON%/python.exe C:/get-pip.py"
- "%PYTHON%/Scripts/pip.exe install -U setuptools"
- "%PYTHON%/python.exe setup.py develop"
- "%PYTHON%/Scripts/pip.exe install -U -r requirements.txt"
test_script:
- "%PYTHON%/python.exe -m flake8"
- "%PYTHON%/Scripts/py.test.exe -sv"

View File

@@ -31,11 +31,12 @@ elif (3, 0) < version < (3, 5):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.30'
VERSION = '3.31'
install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte']
extras_require = {':python_version<"3.4"': ['pathlib2'],
':python_version<"3.3"': ['backports.shutil_get_terminal_size'],
':python_version<="2.7"': ['decorator<5'],
":sys_platform=='win32'": ['win_unicode_console']}
setup(name='thefuck',

View File

@@ -7,6 +7,10 @@ from thefuck.system import Path
shells.shell = shells.Generic()
def pytest_configure(config):
config.addinivalue_line("markers", "functional: mark test as functional")
def pytest_addoption(parser):
"""Adds `--enable-functional` argument."""
group = parser.getgroup("thefuck")

11
tests/rules/test_cd_cs.py Normal file
View File

@@ -0,0 +1,11 @@
from thefuck.rules.cd_cs import match, get_new_command
from thefuck.types import Command
def test_match():
assert match(Command('cs', 'cs: command not found'))
assert match(Command('cs /etc/', 'cs: command not found'))
def test_get_new_command():
assert get_new_command(Command('cs /etc/', 'cs: command not found')) == 'cd /etc/'

View File

@@ -0,0 +1,24 @@
import pytest
from thefuck.rules.conda_mistype import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def mistype_response():
return """
CommandNotFoundError: No command 'conda lst'.
Did you mean 'conda list'?
"""
def test_match(mistype_response):
assert match(Command('conda lst', mistype_response))
err_response = 'bash: codna: command not found'
assert not match(Command('codna list', err_response))
def test_get_new_command(mistype_response):
assert (get_new_command(Command('conda lst', mistype_response)) == ['conda list'])

View File

@@ -2,62 +2,54 @@
import pytest
import os
from collections import namedtuple
from thefuck.rules.fix_file import match, get_new_command
from thefuck.types import Command
FixFileTest = namedtuple('FixFileTest', ['script', 'file', 'line', 'col', 'output'])
# (script, file, line, col (or None), output)
tests = (
('gcc a.c', 'a.c', 3, 1,
"""
FixFileTest('gcc a.c', 'a.c', 3, 1, """
a.c: In function 'main':
a.c:3:1: error: expected expression before '}' token
}
^
"""),
('clang a.c', 'a.c', 3, 1,
"""
FixFileTest('clang a.c', 'a.c', 3, 1, """
a.c:3:1: error: expected expression
}
^
"""),
('perl a.pl', 'a.pl', 3, None,
"""
FixFileTest('perl a.pl', 'a.pl', 3, None, """
syntax error at a.pl line 3, at EOF
Execution of a.pl aborted due to compilation errors.
"""),
('perl a.pl', 'a.pl', 2, None,
"""
FixFileTest('perl a.pl', 'a.pl', 2, None, """
Search pattern not terminated at a.pl line 2.
"""),
('sh a.sh', 'a.sh', 2, None,
"""
FixFileTest('sh a.sh', 'a.sh', 2, None, """
a.sh: line 2: foo: command not found
"""),
('zsh a.sh', 'a.sh', 2, None,
"""
FixFileTest('zsh a.sh', 'a.sh', 2, None, """
a.sh:2: command not found: foo
"""),
('bash a.sh', 'a.sh', 2, None,
"""
FixFileTest('bash a.sh', 'a.sh', 2, None, """
a.sh: line 2: foo: command not found
"""),
('rustc a.rs', 'a.rs', 2, 5,
"""
FixFileTest('rustc a.rs', 'a.rs', 2, 5, """
a.rs:2:5: 2:6 error: unexpected token: `+`
a.rs:2 +
^
"""),
('cargo build', 'src/lib.rs', 3, 5,
"""
FixFileTest('cargo build', 'src/lib.rs', 3, 5, """
Compiling test v0.1.0 (file:///tmp/fix-error/test)
src/lib.rs:3:5: 3:6 error: unexpected token: `+`
src/lib.rs:3 +
@@ -67,16 +59,14 @@ Could not compile `test`.
To learn more, run the command again with --verbose.
"""),
('python a.py', 'a.py', 2, None,
"""
FixFileTest('python a.py', 'a.py', 2, None, """
File "a.py", line 2
+
^
SyntaxError: invalid syntax
"""),
('python a.py', 'a.py', 8, None,
"""
FixFileTest('python a.py', 'a.py', 8, None, """
Traceback (most recent call last):
File "a.py", line 8, in <module>
match("foo")
@@ -89,8 +79,7 @@ Traceback (most recent call last):
TypeError: first argument must be string or compiled pattern
"""),
(u'python café.py', u'café.py', 8, None,
u"""
FixFileTest(u'python café.py', u'café.py', 8, None, u"""
Traceback (most recent call last):
File "café.py", line 8, in <module>
match("foo")
@@ -103,57 +92,48 @@ Traceback (most recent call last):
TypeError: first argument must be string or compiled pattern
"""),
('ruby a.rb', 'a.rb', 3, None,
"""
FixFileTest('ruby a.rb', 'a.rb', 3, None, """
a.rb:3: syntax error, unexpected keyword_end
"""),
('lua a.lua', 'a.lua', 2, None,
"""
FixFileTest('lua a.lua', 'a.lua', 2, None, """
lua: a.lua:2: unexpected symbol near '+'
"""),
('fish a.sh', '/tmp/fix-error/a.sh', 2, None,
"""
FixFileTest('fish a.sh', '/tmp/fix-error/a.sh', 2, None, """
fish: Unknown command 'foo'
/tmp/fix-error/a.sh (line 2): foo
^
"""),
('./a', './a', 2, None,
"""
FixFileTest('./a', './a', 2, None, """
awk: ./a:2: BEGIN { print "Hello, world!" + }
awk: ./a:2: ^ syntax error
"""),
('llc a.ll', 'a.ll', 1, 2,
"""
FixFileTest('llc a.ll', 'a.ll', 1, 2, """
llc: a.ll:1:2: error: expected top-level entity
+
^
"""),
('go build a.go', 'a.go', 1, 2,
"""
FixFileTest('go build a.go', 'a.go', 1, 2, """
can't load package:
a.go:1:2: expected 'package', found '+'
"""),
('make', 'Makefile', 2, None,
"""
FixFileTest('make', 'Makefile', 2, None, """
bidule
make: bidule: Command not found
Makefile:2: recipe for target 'target' failed
make: *** [target] Error 127
"""),
('git st', '/home/martin/.config/git/config', 1, None,
"""
FixFileTest('git st', '/home/martin/.config/git/config', 1, None, """
fatal: bad config file line 1 in /home/martin/.config/git/config
"""),
('node fuck.js asdf qwer', '/Users/pablo/Workspace/barebones/fuck.js', '2', 5,
"""
FixFileTest('node fuck.js asdf qwer', '/Users/pablo/Workspace/barebones/fuck.js', '2', 5, """
/Users/pablo/Workspace/barebones/fuck.js:2
conole.log(arg); // this should read console.log(arg);
^
@@ -170,16 +150,14 @@ ReferenceError: conole is not defined
at node.js:814:3
"""),
('pep8', './tests/rules/test_systemctl.py', 17, 80,
"""
FixFileTest('pep8', './tests/rules/test_systemctl.py', 17, 80, """
./tests/rules/test_systemctl.py:17:80: E501 line too long (93 > 79 characters)
./tests/rules/test_systemctl.py:18:80: E501 line too long (103 > 79 characters)
./tests/rules/test_whois.py:20:80: E501 line too long (89 > 79 characters)
./tests/rules/test_whois.py:22:80: E501 line too long (83 > 79 characters)
"""),
('py.test', '/home/thefuck/tests/rules/test_fix_file.py', 218, None,
"""
FixFileTest('py.test', '/home/thefuck/tests/rules/test_fix_file.py', 218, None, """
monkeypatch = <_pytest.monkeypatch.monkeypatch object at 0x7fdb76a25b38>
test = ('fish a.sh', '/tmp/fix-error/a.sh', 2, None, '', "\\nfish: Unknown command 'foo'\\n/tmp/fix-error/a.sh (line 2): foo\\n ^\\n")
@@ -191,7 +169,7 @@ E NameError: name 'mocker' is not defined
/home/thefuck/tests/rules/test_fix_file.py:218: NameError
"""),
) # noqa
)
@pytest.mark.parametrize('test', tests)
@@ -199,7 +177,7 @@ E NameError: name 'mocker' is not defined
def test_match(mocker, monkeypatch, test):
mocker.patch('os.path.isfile', return_value=True)
monkeypatch.setenv('EDITOR', 'dummy_editor')
assert match(Command('', test[4]))
assert match(Command('', test.output))
@pytest.mark.parametrize('test', tests)
@@ -209,7 +187,7 @@ def test_no_editor(mocker, monkeypatch, test):
if 'EDITOR' in os.environ:
monkeypatch.delenv('EDITOR')
assert not match(Command('', test[4]))
assert not match(Command('', test.output))
@pytest.mark.parametrize('test', tests)
@@ -218,7 +196,7 @@ def test_not_file(mocker, monkeypatch, test):
mocker.patch('os.path.isfile', return_value=False)
monkeypatch.setenv('EDITOR', 'dummy_editor')
assert not match(Command('', test[4]))
assert not match(Command('', test.output))
@pytest.mark.parametrize('test', tests)
@@ -234,12 +212,12 @@ def test_get_new_command_with_settings(mocker, monkeypatch, test, settings):
mocker.patch('os.path.isfile', return_value=True)
monkeypatch.setenv('EDITOR', 'dummy_editor')
cmd = Command(test[0], test[4])
cmd = Command(test.script, test.output)
settings.fixcolcmd = '{editor} {file} +{line}:{col}'
if test[3]:
if test.col:
assert (get_new_command(cmd) ==
u'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
u'dummy_editor {} +{}:{} && {}'.format(test.file, test.line, test.col, test.script))
else:
assert (get_new_command(cmd) ==
u'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))
u'dummy_editor {} +{} && {}'.format(test.file, test.line, test.script))

View File

@@ -0,0 +1,24 @@
from thefuck.rules.git_clone_git_clone import match, get_new_command
from thefuck.types import Command
output_clean = """
fatal: Too many arguments.
usage: git clone [<options>] [--] <repo> [<dir>]
"""
def test_match():
assert match(Command('git clone git clone foo', output_clean))
def test_not_match():
assert not match(Command('', ''))
assert not match(Command('git branch', ''))
assert not match(Command('git clone foo', ''))
assert not match(Command('git clone foo bar baz', output_clean))
def test_get_new_command():
assert get_new_command(Command('git clone git clone foo', output_clean)) == 'git clone foo'

View File

@@ -0,0 +1,43 @@
import pytest
from thefuck.rules.git_hook_bypass import match, get_new_command
from thefuck.types import Command
@pytest.mark.parametrize(
"command",
[
Command("git am", ""),
Command("git commit", ""),
Command("git commit -m 'foo bar'", ""),
Command("git push", ""),
Command("git push -u foo bar", ""),
],
)
def test_match(command):
assert match(command)
@pytest.mark.parametrize(
"command",
[
Command("git add foo", ""),
Command("git status", ""),
Command("git diff foo bar", ""),
],
)
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize(
"command, new_command",
[
(Command("git am", ""), "git am --no-verify"),
(Command("git commit", ""), "git commit --no-verify"),
(Command("git commit -m 'foo bar'", ""), "git commit --no-verify -m 'foo bar'"),
(Command("git push", ""), "git push --no-verify"),
(Command("git push -p", ""), "git push --no-verify -p"),
],
)
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -0,0 +1,29 @@
import pytest
from thefuck.rules.git_lfs_mistype import match, get_new_command
from thefuck.types import Command
@pytest.fixture
def mistype_response():
return """
Error: unknown command "evn" for "git-lfs"
Did you mean this?
env
ext
Run 'git-lfs --help' for usage.
"""
def test_match(mistype_response):
assert match(Command('git lfs evn', mistype_response))
err_response = 'bash: git: command not found'
assert not match(Command('git lfs env', err_response))
assert not match(Command('docker lfs env', mistype_response))
def test_get_new_command(mistype_response):
assert (get_new_command(Command('git lfs evn', mistype_response))
== ['git lfs env', 'git lfs ext'])

View File

@@ -1,4 +1,5 @@
import pytest
from io import BytesIO
from thefuck.rules.go_unknown_command import match, get_new_command
from thefuck.types import Command
@@ -9,6 +10,65 @@ def build_misspelled_output():
Run 'go help' for usage.'''
@pytest.fixture
def go_stderr(mocker):
stderr = b'''Go is a tool for managing Go source code.
Usage:
\tgo <command> [arguments]
The commands are:
\tbug start a bug report
\tbuild compile packages and dependencies
\tclean remove object files and cached files
\tdoc show documentation for package or symbol
\tenv print Go environment information
\tfix update packages to use new APIs
\tfmt gofmt (reformat) package sources
\tgenerate generate Go files by processing source
\tget add dependencies to current module and install them
\tinstall compile and install packages and dependencies
\tlist list packages or modules
\tmod module maintenance
\trun compile and run Go program
\ttest test packages
\ttool run specified go tool
\tversion print Go version
\tvet report likely mistakes in packages
Use "go help <command>" for more information about a command.
Additional help topics:
\tbuildconstraint build constraints
\tbuildmode build modes
\tc calling between Go and C
\tcache build and test caching
\tenvironment environment variables
\tfiletype file types
\tgo.mod the go.mod file
\tgopath GOPATH environment variable
\tgopath-get legacy GOPATH go get
\tgoproxy module proxy protocol
\timportpath import path syntax
\tmodules modules, module versions, and more
\tmodule-get module-aware go get
\tmodule-auth module authentication using go.sum
\tmodule-private module configuration for non-public modules
\tpackages package lists and patterns
\ttestflag testing flags
\ttestfunc testing functions
Use "go help <topic>" for more information about that topic.
'''
mock = mocker.patch('subprocess.Popen')
mock.return_value.stderr = BytesIO(stderr)
return mock
def test_match(build_misspelled_output):
assert match(Command('go bulid', build_misspelled_output))
@@ -17,5 +77,6 @@ def test_not_match():
assert not match(Command('go run', 'go run: no go files listed'))
@pytest.mark.usefixtures('no_memoize', 'go_stderr')
def test_get_new_command(build_misspelled_output):
assert get_new_command(Command('go bulid', build_misspelled_output)) == 'go build'

View File

@@ -1,6 +1,6 @@
import pytest
from thefuck.rules.pyenv_no_such_command import get_new_command, match
from thefuck.rules.omnienv_no_such_command import get_new_command, match
from thefuck.types import Command
@@ -11,7 +11,7 @@ def output(pyenv_cmd):
@pytest.fixture(autouse=True)
def Popen(mocker):
mock = mocker.patch('thefuck.rules.pyenv_no_such_command.Popen')
mock = mocker.patch('thefuck.rules.omnienv_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'
@@ -33,6 +33,11 @@ def test_match(script, pyenv_cmd, output):
assert match(Command(script, output=output))
def test_match_goenv_output_quote():
"""test goenv's specific output with quotes (')"""
assert match(Command('goenv list', output="goenv: no such command 'list'"))
@pytest.mark.parametrize('script, output', [
('pyenv global', 'system'),
('pyenv versions', ' 3.7.0\n 3.7.1\n* 3.7.2\n'),

View File

@@ -0,0 +1,30 @@
import pytest
from thefuck.rules.pacman_invalid_option import get_new_command, match
from thefuck.types import Command
good_output = """community/shared_meataxe 1.0-3
A set of programs for working with matrix representations over finite fields
"""
bad_output = "error: invalid option '-"
@pytest.mark.parametrize("option", "SURQFDVT")
def test_not_match_good_output(option):
assert not match(Command("pacman -{}s meat".format(option), good_output))
@pytest.mark.parametrize("option", "azxcbnm")
def test_not_match_bad_output(option):
assert not match(Command("pacman -{}v meat".format(option), bad_output))
@pytest.mark.parametrize("option", "surqfdvt")
def test_match(option):
assert match(Command("pacman -{}v meat".format(option), bad_output))
@pytest.mark.parametrize("option", "surqfdvt")
def test_get_new_command(option):
new_command = get_new_command(Command("pacman -{}v meat".format(option), ""))
assert new_command == "pacman -{}v meat".format(option.upper())

View File

@@ -0,0 +1,63 @@
import pytest
from thefuck.rules.python_module_error import get_new_command, match
from thefuck.types import Command
@pytest.fixture
def module_error_output(filename, module_name):
return """Traceback (most recent call last):
File "{0}", line 1, in <module>
import {1}
ModuleNotFoundError: No module named '{1}'""".format(
filename, module_name
)
@pytest.mark.parametrize(
"test",
[
Command("python hello_world.py", "Hello World"),
Command(
"./hello_world.py",
"""Traceback (most recent call last):
File "hello_world.py", line 1, in <module>
pritn("Hello World")
NameError: name 'pritn' is not defined""",
),
],
)
def test_not_match(test):
assert not match(test)
positive_tests = [
(
"python some_script.py",
"some_script.py",
"more_itertools",
"pip install more_itertools && python some_script.py",
),
(
"./some_other_script.py",
"some_other_script.py",
"a_module",
"pip install a_module && ./some_other_script.py",
),
]
@pytest.mark.parametrize(
"script, filename, module_name, corrected_script", positive_tests
)
def test_match(script, filename, module_name, corrected_script, module_error_output):
assert match(Command(script, module_error_output))
@pytest.mark.parametrize(
"script, filename, module_name, corrected_script", positive_tests
)
def test_get_new_command(
script, filename, module_name, corrected_script, module_error_output
):
assert get_new_command(Command(script, module_error_output)) == corrected_script

View File

@@ -43,7 +43,7 @@ class TestSettingsFromFile(object):
assert settings.rules == const.DEFAULT_RULES + ['test']
@pytest.mark.usefixture('load_source')
@pytest.mark.usefixtures('load_source')
class TestSettingsFromEnv(object):
def test_from_env(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'bash:lisp',
@@ -54,7 +54,8 @@ class TestSettingsFromEnv(object):
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
'THEFUCK_WAIT_SLOW_COMMAND': '999',
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew',
'THEFUCK_NUM_CLOSE_MATCHES': '359'})
'THEFUCK_NUM_CLOSE_MATCHES': '359',
'THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES': '/media/:/mnt/'})
settings.init()
assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim']
@@ -65,6 +66,7 @@ class TestSettingsFromEnv(object):
assert settings.wait_slow_command == 999
assert settings.slow_commands == ['lein', 'react-native', './gradlew']
assert settings.num_close_matches == 359
assert settings.excluded_search_path_prefixes == ['/media/', '/mnt/']
def test_from_env_with_DEFAULT(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@@ -8,14 +8,15 @@ from thefuck.types import Command
from thefuck.corrector import get_corrected_commands, organize_commands
class TestGetRules(object):
@pytest.fixture
def glob(self, mocker):
results = {}
mocker.patch('thefuck.system.Path.glob',
new_callable=lambda: lambda *_: results.pop('value', []))
return lambda value: results.update({'value': value})
@pytest.fixture
def glob(mocker):
results = {}
mocker.patch('thefuck.system.Path.glob',
new_callable=lambda: lambda *_: results.pop('value', []))
return lambda value: results.update({'value': value})
class TestGetRules(object):
@pytest.fixture(autouse=True)
def load_source(self, monkeypatch):
monkeypatch.setattr('thefuck.types.load_source',
@@ -39,6 +40,14 @@ class TestGetRules(object):
self._compare_names(rules, loaded_rules)
def test_get_rules_rule_exception(mocker, glob):
load_source = mocker.patch('thefuck.types.load_source',
side_effect=ImportError("No module named foo..."))
glob([Path('git.py')])
assert not corrector.get_rules()
load_source.assert_called_once_with('git', 'git.py')
def test_get_corrected_commands(mocker):
command = Command('test', 'test')
rules = [Rule(match=lambda _: False),

View File

@@ -41,10 +41,16 @@ class TestCorrectedCommand(object):
settings.update(override_settings)
CorrectedCommand(script, None, 1000).run(Command(script, ''))
out, _ = capsys.readouterr()
assert out[:-1] == printed
assert out == printed
class TestRule(object):
def test_from_path_rule_exception(self, mocker):
load_source = mocker.patch('thefuck.types.load_source',
side_effect=ImportError("No module named foo..."))
assert Rule.from_path(Path('git.py')) is None
load_source.assert_called_once_with('git', 'git.py')
def test_from_path(self, mocker):
match = object()
get_new_command = object()
@@ -60,20 +66,22 @@ class TestRule(object):
== Rule('bash', match, get_new_command, priority=900))
load_source.assert_called_once_with('bash', rule_path)
@pytest.mark.parametrize('rules, exclude_rules, rule, is_enabled', [
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=True), False),
(const.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
(['git'], [], Rule('git', enabled_by_default=False), True),
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
([], ['git'], Rule('git', enabled_by_default=True), False),
([], ['git'], Rule('git', enabled_by_default=False), False)])
def test_is_enabled(self, settings, rules, exclude_rules, rule, is_enabled):
settings.update(rules=rules,
exclude_rules=exclude_rules)
def test_from_path_excluded_rule(self, mocker, settings):
load_source = mocker.patch('thefuck.types.load_source')
settings.update(exclude_rules=['git'])
rule_path = os.path.join(os.sep, 'rules', 'git.py')
assert Rule.from_path(Path(rule_path)) is None
assert not load_source.called
@pytest.mark.parametrize('rules, rule, is_enabled', [
(const.DEFAULT_RULES, Rule('git', enabled_by_default=True), True),
(const.DEFAULT_RULES, Rule('git', enabled_by_default=False), False),
([], Rule('git', enabled_by_default=False), False),
([], Rule('git', enabled_by_default=True), False),
(const.DEFAULT_RULES + ['git'], Rule('git', enabled_by_default=False), True),
(['git'], Rule('git', enabled_by_default=False), True)])
def test_is_enabled(self, settings, rules, rule, is_enabled):
settings.update(rules=rules)
assert rule.is_enabled == is_enabled
def test_isnt_match(self):
@@ -131,6 +139,7 @@ class TestCommand(object):
env=os_environ)
@pytest.mark.parametrize('script, result', [
([], None),
([''], None),
(['', ''], None),
(['ls', '-la'], 'ls -la'),

View File

@@ -94,6 +94,20 @@ def test_get_all_executables_pathsep(path, pathsep):
Path_mock.assert_has_calls([call(p) for p in path.split(pathsep)], True)
@pytest.mark.usefixtures('no_memoize', 'os_environ_pathsep')
@pytest.mark.parametrize('path, pathsep, excluded', [
('/foo:/bar:/baz:/foo/bar:/mnt/foo', ':', '/mnt/foo'),
(r'C:\\foo;C:\\bar;C:\\baz;C:\\foo\\bar;Z:\\foo', ';', r'Z:\\foo')])
def test_get_all_executables_exclude_paths(path, pathsep, excluded, settings):
settings.init()
settings.excluded_search_path_prefixes = [excluded]
with patch('thefuck.utils.Path') as Path_mock:
get_all_executables()
path_list = path.split(pathsep)
assert call(path_list[-1]) not in Path_mock.mock_calls
assert all(call(p) in Path_mock.mock_calls for p in path_list[:-1])
@pytest.mark.parametrize('args, result', [
(('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'),
(('git brnch', 'brnch', 'branch'), 'git branch')])

View File

@@ -101,7 +101,7 @@ class Settings(dict):
elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history', 'instant_mode'):
return val.lower() == 'true'
elif attr == 'slow_commands':
elif attr in ('slow_commands', 'excluded_search_path_prefixes'):
return val.split(':')
else:
return val

View File

@@ -43,7 +43,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'repeat': False,
'instant_mode': False,
'num_close_matches': 3,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'},
'excluded_search_path_prefixes': []}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_EXCLUDE_RULES': 'exclude_rules',
@@ -58,7 +59,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode',
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches'}
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches',
'THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES': 'excluded_search_path_prefixes'}
SETTINGS_HEADER = u"""# The Fuck settings file
#

View File

@@ -15,7 +15,7 @@ def get_loaded_rules(rules_paths):
for path in rules_paths:
if path.name != '__init__.py':
rule = Rule.from_path(path)
if rule.is_enabled:
if rule and rule.is_enabled:
yield rule
@@ -71,7 +71,7 @@ def organize_commands(corrected_commands):
without_duplicates,
key=lambda corrected_command: corrected_command.priority)
logs.debug('Corrected commands: '.format(
logs.debug(u'Corrected commands: {}'.format(
', '.join(u'{}'.format(cmd) for cmd in [first_command] + sorted_commands)))
for command in sorted_commands:

View File

@@ -23,6 +23,7 @@ def _get_raw_command(known_args):
diff = SequenceMatcher(a=alias, b=command).ratio()
if diff < const.DIFF_WITH_ALIAS or command in executables:
return [command]
return []
def fix_command(known_args):

View File

@@ -3,8 +3,8 @@ import re
from thefuck.utils import get_closest, replace_command
from thefuck.specific.brew import get_brew_path_prefix, brew_available
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
BREW_CMD_PATH = '/Homebrew/Library/Homebrew/cmd'
TAP_PATH = '/Homebrew/Library/Taps'
TAP_CMD_PATH = '/%s/%s/cmd'
enabled_by_default = brew_available
@@ -62,7 +62,7 @@ def _brew_commands():
# Failback commands for testing (Based on Homebrew 0.9.5)
return ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
'doctor', 'create', 'edit', 'cask']
def match(command):

21
thefuck/rules/cd_cs.py Normal file
View File

@@ -0,0 +1,21 @@
# -*- encoding: utf-8 -*-
# Redirects cs to cd when there is a typo
# Due to the proximity of the keys - d and s - this seems like a common typo
# ~ > cs /etc/
# cs: command not found
# ~ > fuck
# cd /etc/ [enter/↑/↓/ctrl+c]
# /etc >
def match(command):
if command.script_parts[0] == 'cs':
return True
def get_new_command(command):
return 'cd' + ''.join(command.script[2:])
priority = 900

View File

@@ -0,0 +1,17 @@
import re
from thefuck.utils import replace_command, for_app
@for_app("conda")
def match(command):
"""
Match a mistyped command
"""
return "Did you mean 'conda" in command.output
def get_new_command(command):
match = re.findall(r"'conda ([^']*)'", command.output)
broken_cmd = match[0]
correct_cmd = match[1]
return replace_command(command, broken_cmd, [correct_cmd])

View File

@@ -41,6 +41,10 @@ def get_new_command(command):
def side_effect(old_cmd, command):
with tarfile.TarFile(_tar_file(old_cmd.script_parts)[0]) as archive:
for file in archive.getnames():
if not os.path.abspath(file).startswith(os.getcwd()):
# it's unsafe to overwrite files outside of the current directory
continue
try:
os.remove(file)
except OSError:

View File

@@ -45,6 +45,10 @@ def get_new_command(command):
def side_effect(old_cmd, command):
with zipfile.ZipFile(_zip_file(old_cmd), 'r') as archive:
for file in archive.namelist():
if not os.path.abspath(file).startswith(os.getcwd()):
# it's unsafe to overwrite files outside of the current directory
continue
try:
os.remove(file)
except OSError:

View File

@@ -0,0 +1,12 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return (' git clone ' in command.script
and 'fatal: Too many arguments.' in command.output)
@git_support
def get_new_command(command):
return command.script.replace(' git clone ', ' ', 1)

View File

@@ -0,0 +1,27 @@
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
hooked_commands = ("am", "commit", "push")
@git_support
def match(command):
return any(
hooked_command in command.script_parts for hooked_command in hooked_commands
)
@git_support
def get_new_command(command):
hooked_command = next(
hooked_command
for hooked_command in hooked_commands
if hooked_command in command.script_parts
)
return replace_argument(
command.script, hooked_command, hooked_command + " --no-verify"
)
priority = 900
requires_output = False

View File

@@ -0,0 +1,18 @@
import re
from thefuck.utils import get_all_matched_commands, replace_command
from thefuck.specific.git import git_support
@git_support
def match(command):
'''
Match a mistyped command
'''
return 'lfs' in command.script and 'Did you mean this?' in command.output
@git_support
def get_new_command(command):
broken_cmd = re.findall(r'Error: unknown command "([^"]*)" for "git-lfs"', command.output)[0]
matched = get_all_matched_commands(command.output, ['Did you mean', ' for usage.'])
return replace_command(command, broken_cmd, matched)

View File

@@ -1,8 +1,12 @@
import re
from subprocess import PIPE, Popen
from thefuck.utils import (cache, for_app, replace_argument, replace_command,
which)
from subprocess import PIPE, Popen
supported_apps = 'goenv', 'nodenv', 'pyenv', 'rbenv'
enabled_by_default = any(which(a) for a in supported_apps)
COMMON_TYPOS = {
'list': ['versions', 'install --list'],
@@ -10,24 +14,22 @@ COMMON_TYPOS = {
}
@for_app('pyenv')
@for_app(*supported_apps, at_least=1)
def match(command):
return 'pyenv: no such command' in command.output
return 'env: no such command ' in command.output
def get_pyenv_commands():
proc = Popen(['pyenv', 'commands'], stdout=PIPE)
def get_app_commands(app):
proc = Popen([app, '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]
broken = re.findall(r"env: 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()))
app = command.script_parts[0]
app_commands = cache(which(app))(get_app_commands)(app)
matched.extend(replace_command(command, broken, app_commands))
return matched

View File

@@ -0,0 +1,20 @@
from thefuck.specific.archlinux import archlinux_env
from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app
import re
@sudo_support
@for_app("pacman")
def match(command):
return command.output.startswith("error: invalid option '-") and any(
" -{}".format(option) in command.script for option in "surqfdvt"
)
def get_new_command(command):
option = re.findall(r" -[dfqrstuv]", command.script)[0]
return re.sub(option, option.upper(), command.script)
enabled_by_default = archlinux_env()

View File

@@ -0,0 +1,13 @@
import re
from thefuck.shells import shell
MISSING_MODULE = r"ModuleNotFoundError: No module named '([^']+)'"
def match(command):
return "ModuleNotFoundError: No module named '" in command.output
def get_new_command(command):
missing_module = re.findall(MISSING_MODULE, command.output)[0]
return shell.and_("pip install {}".format(missing_module), command.script)

View File

@@ -122,7 +122,7 @@ class Rule(object):
def __repr__(self):
return 'Rule(name={}, match={}, get_new_command={}, ' \
'enabled_by_default={}, side_effect={}, ' \
'priority={}, requires_output)'.format(
'priority={}, requires_output={})'.format(
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
@@ -136,9 +136,16 @@ class Rule(object):
"""
name = path.name[:-3]
if name in settings.exclude_rules:
logs.debug(u'Ignoring excluded rule: {}'.format(name))
return
with logs.debug_time(u'Importing rule: {};'.format(name)):
rule_module = load_source(name, str(path))
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
try:
rule_module = load_source(name, str(path))
except Exception:
logs.exception(u"Rule {} failed to load".format(name), sys.exc_info())
return
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
return cls(name, rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
@@ -153,14 +160,11 @@ class Rule(object):
:rtype: bool
"""
if self.name in settings.exclude_rules:
return False
elif self.name in settings.rules:
return True
elif self.enabled_by_default and ALL_ENABLED in settings.rules:
return True
else:
return False
return (
self.name in settings.rules
or self.enabled_by_default
and ALL_ENABLED in settings.rules
)
def is_match(self, command):
"""Returns `True` if rule matches the command.
@@ -255,4 +259,4 @@ class CorrectedCommand(object):
logs.debug(u'PYTHONIOENCODING: {}'.format(
os.environ.get('PYTHONIOENCODING', '!!not-set!!')))
print(self._get_script())
sys.stdout.write(self._get_script())

View File

@@ -104,6 +104,10 @@ def get_close_matches(word, possibilities, n=None, cutoff=0.6):
return difflib_get_close_matches(word, possibilities, n, cutoff)
def include_path_in_search(path):
return not any(path.startswith(x) for x in settings.excluded_search_path_prefixes)
@memoize
def get_all_executables():
from thefuck.shells import shell
@@ -119,6 +123,7 @@ def get_all_executables():
bins = [exe.name.decode('utf8') if six.PY2 else exe.name
for path in os.environ.get('PATH', '').split(os.pathsep)
if include_path_in_search(path)
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)
and exe.name not in tf_entry_points]