mirror of
https://github.com/nvbn/thefuck.git
synced 2025-11-01 15:42:06 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0949d2e770 | ||
|
|
e343c577cd | ||
|
|
6da0bc557f | ||
|
|
1a595f1ba2 | ||
|
|
875d3f11cb | ||
|
|
4c7479b3ad | ||
|
|
5b612add74 | ||
|
|
b9dd54c768 | ||
|
|
7af9f41d93 | ||
|
|
c2cc95db88 | ||
|
|
0e34c2343e | ||
|
|
fd90e69ceb | ||
|
|
0c58317932 | ||
|
|
62dddd5821 | ||
|
|
40dd65963d | ||
|
|
836f6eeac5 | ||
|
|
b4c75eebe6 | ||
|
|
22efa8f70e | ||
|
|
9d3bcad229 | ||
|
|
c196e2901c | ||
|
|
ca46956e20 | ||
|
|
639e9bda7a | ||
|
|
39753a004e | ||
|
|
365db1ee41 | ||
|
|
81b39defe4 | ||
|
|
f82176802e | ||
|
|
6975d30818 | ||
|
|
3c542a5b8c |
49
.github/workflows/test.yml
vendored
Normal file
49
.github/workflows/test.yml
vendored
Normal 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
|
||||
51
.travis.yml
51
.travis.yml
@@ -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
|
||||
@@ -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
|
||||
|
||||
50
README.md
50
README.md
@@ -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` – fixes wrongs commands like `cargo buid`;
|
||||
* `cat_dir` – replaces `cat` with `ls` when you try to `cat` a directory;
|
||||
* `cd_correction` – spellchecks and correct failed cd commands;
|
||||
* `cd_cs` – changes `cs` to `cd`;
|
||||
* `cd_mkdir` – creates directories before cd'ing into them;
|
||||
* `cd_parent` – changes `cd..` to `cd ..`;
|
||||
* `chmod_x` – add execution bit;
|
||||
* `choco_install` – append common suffixes for chocolatey packages;
|
||||
* `composer_not_command` – fixes composer command name;
|
||||
* `conda_mistype` – fixes conda commands;
|
||||
* `cp_create_destination` – creates a new directory when you attempt to `cp` or `mv` to a non existent one
|
||||
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
|
||||
* `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`;
|
||||
@@ -207,6 +226,7 @@ following rules are enabled by default:
|
||||
* `git_branch_exists` – offers `git branch -d foo`, `git branch -D foo` or `git checkout foo` when creating a branch that already exists;
|
||||
* `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch;
|
||||
* `git_checkout` – fixes branch name or creates new branch;
|
||||
* `git_clone_git_clone` – replaces `git clone git clone ...` with `git clone ...`
|
||||
* `git_commit_amend` – offers `git commit --amend` after previous commit;
|
||||
* `git_commit_reset` – offers `git reset HEAD~` after previous commit;
|
||||
* `git_diff_no_index` – adds `--no-index` to previous `git diff` on untracked files;
|
||||
@@ -214,6 +234,8 @@ following rules are enabled by default:
|
||||
* `git_fix_stash` – fixes `git stash` commands (misspelled subcommand and missing `save`);
|
||||
* `git_flag_after_filename` – fixes `fatal: bad flag '...' after filename`
|
||||
* `git_help_aliased` – fixes `git help <alias>` commands replacing <alias> with the aliased command;
|
||||
* `git_hook_bypass` – adds `--no-verify` flag previous to `git am`, `git commit`, or `git push` command;
|
||||
* `git_lfs_mistype` – fixes mistyped `git lfs <command>` commands;
|
||||
* `git_merge` – adds remote to branch names;
|
||||
* `git_merge_unrelated` – adds `--allow-unrelated-histories` when required
|
||||
* `git_not_command` – fixes wrong git commands like `git brnch`;
|
||||
@@ -269,15 +291,16 @@ following rules are enabled by default:
|
||||
* `npm_wrong_command` – fixes wrong npm commands like `npm urgrade`;
|
||||
* `no_command` – fixes wrong console commands, for example `vom/vim`;
|
||||
* `no_such_file` – creates missing directories with `mv` and `cp` commands;
|
||||
* `omnienv_no_such_command` – fixes wrong commands for `goenv`, `nodenv`, `pyenv` and `rbenv` (eg.: `pyenv isntall` or `goenv list`);
|
||||
* `open` – either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`;
|
||||
* `pip_install` – fixes permission issues with `pip install` commands by adding `--user` or prepending `sudo` if necessary;
|
||||
* `pip_unknown_command` – fixes wrong `pip` commands, for example `pip instatl/pip install`;
|
||||
* `php_s` – replaces `-s` by `-S` when trying to run a local php server;
|
||||
* `port_already_in_use` – kills process that bound port;
|
||||
* `prove_recursively` – adds `-r` when called with directory;
|
||||
* `pyenv_no_such_command` – fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`;
|
||||
* `python_command` – prepends `python` when you try to run non-executable/without `./` python script;
|
||||
* `python_execute` – appends missing `.py` when executing Python files;
|
||||
* `python_module_error` – fixes ModuleNotFoundError by trying to `pip install` that module;
|
||||
* `quotation_marks` – fixes uneven usage of `'` and `"` when containing args';
|
||||
* `path_from_history` – replaces not found path with similar absolute path from history;
|
||||
* `react_native_command_unrecognized` – fixes unrecognized `react-native` commands;
|
||||
@@ -308,6 +331,8 @@ following rules are enabled by default:
|
||||
* `yarn_command_replaced` – fixes replaced `yarn` commands;
|
||||
* `yarn_help` – makes it easier to open `yarn` documentation;
|
||||
|
||||
##### [Back to Contents](#contents)
|
||||
|
||||
The following rules are enabled by default on specific platforms only:
|
||||
|
||||
* `apt_get` – 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` – fixes mistyped DNF commands;
|
||||
* `nixos_cmd_not_found` – installs apps on NixOS;
|
||||
* `pacman` – installs app with `pacman` if it is not installed (uses `yay` or `yaourt` if available);
|
||||
* `pacman_invalid_option` – replaces lowercase `pacman` options with uppercase.
|
||||
* `pacman_not_found` – fixes package name with `pacman`, `yay` or `yaourt`.
|
||||
* `yum_invalid_operation` – fixes invalid `yum` calls, like `yum isntall vim`;
|
||||
|
||||
@@ -334,6 +360,8 @@ default:
|
||||
* `git_push_force` – adds `--force-with-lease` to a `git push` (may conflict with `git_push_pull`);
|
||||
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
|
||||
|
||||
##### [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` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
|
||||
* `slow_commands` – list of slow commands;
|
||||
* `num_close_matches` – maximum number of close matches to suggest, by default `3`.
|
||||
* `excluded_search_path_prefixes` – 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` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
|
||||
* `THEFUCK_SLOW_COMMANDS` – list of slow commands, like `lein:gradle`;
|
||||
* `THEFUCK_NUM_CLOSE_MATCHES` – maximum number of close matches to suggest, like `5`.
|
||||
* `THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES` – 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)
|
||||
|
||||
23
appveyor.yml
23
appveyor.yml
@@ -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"
|
||||
3
setup.py
3
setup.py
@@ -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',
|
||||
|
||||
@@ -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
11
tests/rules/test_cd_cs.py
Normal 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/'
|
||||
24
tests/rules/test_conda_mistype.py
Normal file
24
tests/rules/test_conda_mistype.py
Normal 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'])
|
||||
@@ -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))
|
||||
|
||||
24
tests/rules/test_git_clone_git_clone.py
Normal file
24
tests/rules/test_git_clone_git_clone.py
Normal 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'
|
||||
43
tests/rules/test_git_hook_bypass.py
Normal file
43
tests/rules/test_git_hook_bypass.py
Normal 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
|
||||
29
tests/rules/test_git_lfs_mistype.py
Normal file
29
tests/rules/test_git_lfs_mistype.py
Normal 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'])
|
||||
@@ -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'
|
||||
|
||||
@@ -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'),
|
||||
30
tests/rules/test_pacman_invalid_option.py
Normal file
30
tests/rules/test_pacman_invalid_option.py
Normal 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())
|
||||
63
tests/rules/test_python_module_error.py
Normal file
63
tests/rules/test_python_module_error.py
Normal 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
|
||||
@@ -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'})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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')])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
21
thefuck/rules/cd_cs.py
Normal 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
|
||||
17
thefuck/rules/conda_mistype.py
Normal file
17
thefuck/rules/conda_mistype.py
Normal 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])
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
12
thefuck/rules/git_clone_git_clone.py
Normal file
12
thefuck/rules/git_clone_git_clone.py
Normal 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)
|
||||
27
thefuck/rules/git_hook_bypass.py
Normal file
27
thefuck/rules/git_hook_bypass.py
Normal 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
|
||||
18
thefuck/rules/git_lfs_mistype.py
Normal file
18
thefuck/rules/git_lfs_mistype.py
Normal 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)
|
||||
@@ -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
|
||||
20
thefuck/rules/pacman_invalid_option.py
Normal file
20
thefuck/rules/pacman_invalid_option.py
Normal 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()
|
||||
13
thefuck/rules/python_module_error.py
Normal file
13
thefuck/rules/python_module_error.py
Normal 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)
|
||||
@@ -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())
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user