├── tests ├── __init__.py ├── rules │ ├── __init__.py │ ├── test_sl_ls.py │ ├── test_cd_parent.py │ ├── test_man_no_space.py │ ├── test_cd_cs.py │ ├── test_python_command.py │ ├── test_ls_all.py │ ├── test_git_tag_force.py │ ├── test_grep_recursive.py │ ├── test_go_run.py │ ├── test_java.py │ ├── test_javac.py │ ├── test_python_execute.py │ ├── test_dry.py │ ├── test_git_stash_pop.py │ ├── test_ls_lah.py │ ├── test_rm_root.py │ ├── test_git_pull_uncommitted_changes.py │ ├── test_lein_not_task.py │ ├── test_git_branch_list.py │ ├── test_git_pull_unstaged_changes.py │ ├── test_remove_trailing_cedilla.py │ ├── test_tmux.py │ ├── test_conda_mistype.py │ ├── test_git_branch_delete.py │ ├── test_long_form_help.py │ ├── test_has_exists_script.py │ ├── test_git_add_force.py │ ├── test_git_clone_git_clone.py │ ├── test_cp_omitting_directory.py │ ├── test_git_pull_clone.py │ ├── test_unsudo.py │ ├── test_git_diff_no_index.py │ ├── test_git_commit_amend.py │ ├── test_git_commit_reset.py │ ├── test_git_remote_delete.py │ ├── test_git_push_without_commits.py │ ├── test_fix_alt_space.py │ ├── test_ag_literal.py │ ├── test_apt_get_search.py │ ├── test_quotation_marks.py │ ├── test_cd_correction.py │ ├── test_git_lfs_mistype.py │ ├── test_git_diff_staged.py │ ├── test_php_s.py │ ├── test_history.py │ ├── test_cargo_no_command.py │ ├── test_yarn_alias.py │ ├── test_git_merge.py │ ├── test_git_pull.py │ ├── test_brew_update_formula.py │ ├── test_git_merge_unrelated.py │ ├── test_brew_reinstall.py │ ├── test_hostscli.py │ ├── test_git_rm_recursive.py │ ├── test_heroku_not_command.py │ ├── test_systemctl.py │ ├── test_gulp_not_task.py │ ├── test_nixos_cmd_not_found.py │ ├── test_brew_unknown_command.py │ ├── test_cd_mkdir.py │ ├── test_git_remote_seturl_add.py │ ├── test_wrong_hyphen_before_subcommand.py │ ├── test_rm_dir.py │ ├── test_git_branch_delete_checked_out.py │ ├── test_git_rebase_no_changes.py │ ├── test_workon_doesnt_exists.py │ ├── test_git_help_aliased.py │ ├── test_whois.py │ ├── test_missing_space_before_subcommand.py │ ├── test_touch.py │ ├── test_no_such_file.py │ ├── test_brew_cask_dependency.py │ ├── test_git_rm_staged.py │ ├── test_git_bisect_usage.py │ ├── test_git_rm_local_modifications.py │ ├── test_pacman_invalid_option.py │ ├── test_cp_create_destination.py │ ├── test_yarn_command_replaced.py │ ├── test_brew_uninstall.py │ ├── test_git_commit_add.py │ ├── test_git_push_different_branch_names.py │ ├── test_ln_s_order.py │ ├── test_tsuru_login.py │ ├── test_pip_unknown_command.py │ ├── test_git_fix_stash.py │ ├── test_sed_unterminated_s.py │ ├── test_git_stash.py │ └── test_mkdir_p.py ├── functional │ ├── __init__.py │ ├── conftest.py │ ├── test_fish.py │ └── test_tcsh.py ├── shells │ ├── __init__.py │ └── conftest.py ├── specific │ ├── __init__.py │ ├── test_sudo.py │ └── test_npm.py ├── entrypoints │ ├── __init__.py │ └── test_fix_command.py ├── Dockerfile ├── test_readme.py ├── test_logs.py └── utils.py ├── thefuck ├── __init__.py ├── rules │ ├── __init__.py │ ├── cargo.py │ ├── remove_trailing_cedilla.py │ ├── ls_all.py │ ├── test.py.py │ ├── ag_literal.py │ ├── git_commit_amend.py │ ├── git_commit_reset.py │ ├── grep_recursive.py │ ├── man_no_space.py │ ├── sl_ls.py │ ├── django_south_merge.py │ ├── heroku_not_command.py │ ├── quotation_marks.py │ ├── django_south_ghost.py │ ├── git_remote_delete.py │ ├── ls_lah.py │ ├── mvn_no_command.py │ ├── php_s.py │ ├── yarn_command_replaced.py │ ├── cat_dir.py │ ├── git_clone_git_clone.py │ ├── tsuru_login.py │ ├── mkdir_p.py │ ├── git_merge_unrelated.py │ ├── java.py │ ├── git_push_without_commits.py │ ├── git_rebase_no_changes.py │ ├── has_exists_script.py │ ├── apt_get_search.py │ ├── fix_alt_space.py │ ├── git_help_aliased.py │ ├── git_remote_seturl_add.py │ ├── cpp11.py │ ├── git_diff_staged.py │ ├── cd_parent.py │ ├── git_tag_force.py │ ├── gradle_wrapper.py │ ├── heroku_multiple_apps.py │ ├── brew_update_formula.py │ ├── docker_login.py │ ├── git_branch_delete.py │ ├── python_execute.py │ ├── git_add_force.py │ ├── go_run.py │ ├── git_push_different_branch_names.py │ ├── terraform_init.py │ ├── touch.py │ ├── cp_omitting_directory.py │ ├── unsudo.py │ ├── apt_list_upgradable.py │ ├── yarn_alias.py │ ├── git_main_master.py │ ├── javac.py │ ├── git_two_dashes.py │ ├── git_branch_list.py │ ├── python_module_error.py │ ├── rails_migrations_pending.py │ ├── nixos_cmd_not_found.py │ ├── apt_upgrade.py │ ├── git_pull_clone.py │ ├── rm_dir.py │ ├── brew_uninstall.py │ ├── history.py │ ├── git_pull_uncommitted_changes.py │ ├── conda_mistype.py │ ├── git_stash.py │ ├── cargo_no_command.py │ ├── dry.py │ ├── rm_root.py │ ├── chmod_x.py │ ├── cp_create_destination.py │ ├── git_commit_add.py │ ├── brew_link.py │ ├── cd_cs.py │ ├── yarn_help.py │ ├── sed_unterminated_s.py │ ├── git_rm_recursive.py │ ├── pacman.py │ ├── unknown_command.py │ ├── git_diff_no_index.py │ ├── git_pull.py │ ├── terraform_no_command.py │ ├── tmux.py │ ├── git_bisect_usage.py │ ├── git_stash_pop.py │ ├── npm_run_script.py │ ├── python_command.py │ ├── pip_install.py │ ├── wrong_hyphen_before_subcommand.py │ ├── aws_cli.py │ ├── tsuru_not_command.py │ ├── npm_missing_script.py │ ├── git_push_force.py │ ├── ln_no_hard_link.py │ ├── git_branch_delete_checked_out.py │ ├── az_cli.py │ ├── pacman_invalid_option.py │ ├── vagrant_up.py │ ├── git_lfs_mistype.py │ ├── brew_reinstall.py │ ├── remove_shell_prompt_literal.py │ ├── cd_mkdir.py │ ├── git_merge.py │ ├── missing_space_before_subcommand.py │ ├── systemctl.py │ ├── pip_unknown_command.py │ ├── sudo_command_from_user_path.py │ ├── git_rebase_merge_dir.py │ ├── grep_arguments_order.py │ ├── git_rm_staged.py │ ├── git_rm_local_modifications.py │ ├── lein_not_task.py │ ├── gulp_not_task.py │ ├── docker_image_being_used_by_container.py │ ├── git_hook_bypass.py │ ├── git_not_command.py │ ├── prove_recursively.py │ ├── git_push_pull.py │ ├── long_form_help.py │ ├── ifconfig_device_not_found.py │ ├── scm_correction.py │ ├── pacman_not_found.py │ ├── git_add.py │ ├── hostscli.py │ ├── brew_install.py │ ├── ln_s_order.py │ ├── git_branch_0flag.py │ ├── no_such_file.py │ ├── mercurial.py │ ├── workon_doesnt_exists.py │ ├── man.py │ ├── brew_cask_dependency.py │ ├── composer_not_command.py │ ├── choco_install.py │ ├── git_branch_exists.py │ ├── go_unknown_command.py │ ├── git_flag_after_filename.py │ ├── react_native_command_unrecognized.py │ ├── git_fix_stash.py │ ├── gradle_no_task.py │ ├── gem_unknown_command.py │ ├── dnf_no_such_command.py │ ├── grunt_task_not_found.py │ ├── mvn_unknown_lifecycle_phase.py │ ├── fab_command_not_found.py │ ├── omnienv_no_such_command.py │ ├── ssh_known_hosts.py │ ├── port_already_in_use.py │ └── yum_invalid_operation.py ├── specific │ ├── __init__.py │ ├── dnf.py │ ├── nix.py │ ├── yum.py │ ├── apt.py │ ├── brew.py │ ├── sudo.py │ ├── npm.py │ └── git.py ├── entrypoints │ ├── __init__.py │ └── alias.py ├── system │ ├── __init__.py │ └── win32.py ├── exceptions.py └── output_readers │ └── __init__.py ├── setup.cfg ├── MANIFEST.in ├── example.gif ├── example_instant_mode.gif ├── scripts ├── fuck.bat └── fuck.ps1 ├── install.sh ├── requirements.txt ├── .editorconfig ├── tox.ini ├── .devcontainer └── Dockerfile ├── snapcraft.yaml ├── .gitignore ├── release.py ├── LICENSE.md └── .github └── ISSUE_TEMPLATE.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thefuck/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/shells/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/specific/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thefuck/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thefuck/specific/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/entrypoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thefuck/entrypoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include fastentrypoints.py 3 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvbn/thefuck/HEAD/example.gif -------------------------------------------------------------------------------- /example_instant_mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvbn/thefuck/HEAD/example_instant_mode.gif -------------------------------------------------------------------------------- /thefuck/specific/dnf.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import which 2 | 3 | dnf_available = bool(which('dnf')) 4 | -------------------------------------------------------------------------------- /thefuck/specific/nix.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import which 2 | 3 | nix_available = bool(which('nix')) 4 | -------------------------------------------------------------------------------- /thefuck/specific/yum.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import which 2 | 3 | yum_available = bool(which('yum')) 4 | -------------------------------------------------------------------------------- /thefuck/specific/apt.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import which 2 | 3 | apt_available = bool(which('apt-get')) 4 | -------------------------------------------------------------------------------- /scripts/fuck.bat: -------------------------------------------------------------------------------- 1 | @set PYTHONIOENCODING=utf-8 2 | @powershell -noprofile -c "cmd /c \"$(thefuck %* $(doskey /history)[-2])\"; [Console]::ResetColor();" 3 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Installation script is deprecated!" 4 | echo "For installation instruction please visit https://github.com/nvbn/thefuck" 5 | -------------------------------------------------------------------------------- /thefuck/rules/cargo.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | return command.script == 'cargo' 3 | 4 | 5 | def get_new_command(command): 6 | return 'cargo build' 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | mock 4 | pytest-mock 5 | wheel 6 | setuptools>=17.1 7 | pexpect 8 | pypandoc 9 | pytest-benchmark 10 | pytest-docker-pexpect 11 | twine 12 | -------------------------------------------------------------------------------- /thefuck/system/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | if sys.platform == 'win32': 5 | from .win32 import * # noqa: F401,F403 6 | else: 7 | from .unix import * # noqa: F401,F403 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.py] 11 | max_line_length = 119 12 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION 2 | FROM python:${PYTHON_VERSION} 3 | RUN apt-get update -y 4 | RUN apt-get install -yy --no-install-recommends --no-install-suggests fish tcsh zsh 5 | RUN pip install --upgrade pip 6 | COPY . /src 7 | RUN pip install /src 8 | -------------------------------------------------------------------------------- /thefuck/rules/remove_trailing_cedilla.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | CEDILLA = u"ç" 4 | 5 | 6 | def match(command): 7 | return command.script.endswith(CEDILLA) 8 | 9 | 10 | def get_new_command(command): 11 | return command.script[:-1] 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,36,37,38,39,310,311} 3 | 4 | [testenv] 5 | deps = -rrequirements.txt 6 | commands = pytest -v --capture=sys 7 | 8 | [flake8] 9 | ignore = E501,W503,W504 10 | exclude = venv,build,.tox,setup.py,fastentrypoints.py 11 | -------------------------------------------------------------------------------- /thefuck/rules/ls_all.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('ls') 5 | def match(command): 6 | return command.output.strip() == '' 7 | 8 | 9 | def get_new_command(command): 10 | return ' '.join(['ls', '-A'] + command.script_parts[1:]) 11 | -------------------------------------------------------------------------------- /thefuck/rules/test.py.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | return command.script == 'test.py' and 'not found' in command.output 3 | 4 | 5 | def get_new_command(command): 6 | return 'pytest' 7 | 8 | 9 | # make it come before the python_command rule 10 | priority = 900 11 | -------------------------------------------------------------------------------- /thefuck/rules/ag_literal.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('ag') 5 | def match(command): 6 | return command.output.endswith('run ag with -Q\n') 7 | 8 | 9 | def get_new_command(command): 10 | return command.script.replace('ag', 'ag -Q', 1) 11 | -------------------------------------------------------------------------------- /thefuck/rules/git_commit_amend.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return ('commit' in command.script_parts) 7 | 8 | 9 | @git_support 10 | def get_new_command(command): 11 | return 'git commit --amend' 12 | -------------------------------------------------------------------------------- /thefuck/rules/git_commit_reset.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return ('commit' in command.script_parts) 7 | 8 | 9 | @git_support 10 | def get_new_command(command): 11 | return 'git reset HEAD~' 12 | -------------------------------------------------------------------------------- /thefuck/rules/grep_recursive.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('grep') 5 | def match(command): 6 | return 'is a directory' in command.output.lower() 7 | 8 | 9 | def get_new_command(command): 10 | return u'grep -r {}'.format(command.script[5:]) 11 | -------------------------------------------------------------------------------- /thefuck/rules/man_no_space.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | return (command.script.startswith(u'man') 3 | and u'command not found' in command.output.lower()) 4 | 5 | 6 | def get_new_command(command): 7 | return u'man {}'.format(command.script[3:]) 8 | 9 | 10 | priority = 2000 11 | -------------------------------------------------------------------------------- /thefuck/exceptions.py: -------------------------------------------------------------------------------- 1 | class EmptyCommand(Exception): 2 | """Raised when empty command passed to `thefuck`.""" 3 | 4 | 5 | class NoRuleMatched(Exception): 6 | """Raised when no rule matched for some command.""" 7 | 8 | 9 | class ScriptNotInLog(Exception): 10 | """Script not found in log.""" 11 | -------------------------------------------------------------------------------- /thefuck/rules/sl_ls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This happens way too often 3 | 4 | When typing really fast cause I'm a 1337 H4X0R, 5 | I often fuck up 'ls' and type 'sl'. No more! 6 | """ 7 | 8 | 9 | def match(command): 10 | return command.script == 'sl' 11 | 12 | 13 | def get_new_command(command): 14 | return 'ls' 15 | -------------------------------------------------------------------------------- /thefuck/rules/django_south_merge.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | return 'manage.py' in command.script and \ 3 | 'migrate' in command.script \ 4 | and '--merge: will just attempt the migration' in command.output 5 | 6 | 7 | def get_new_command(command): 8 | return u'{} --merge'.format(command.script) 9 | -------------------------------------------------------------------------------- /thefuck/rules/heroku_not_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('heroku') 6 | def match(command): 7 | return 'Run heroku _ to run' in command.output 8 | 9 | 10 | def get_new_command(command): 11 | return re.findall('Run heroku _ to run ([^.]*)', command.output)[0] 12 | -------------------------------------------------------------------------------- /thefuck/rules/quotation_marks.py: -------------------------------------------------------------------------------- 1 | # Fixes careless " and ' usage 2 | # 3 | # Example: 4 | # > git commit -m 'My Message" 5 | 6 | 7 | def match(command): 8 | return '\'' in command.script and '\"' in command.script 9 | 10 | 11 | def get_new_command(command): 12 | return command.script.replace('\'', '\"') 13 | -------------------------------------------------------------------------------- /thefuck/rules/django_south_ghost.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | return 'manage.py' in command.script and \ 3 | 'migrate' in command.script \ 4 | and 'or pass --delete-ghost-migrations' in command.output 5 | 6 | 7 | def get_new_command(command): 8 | return u'{} --delete-ghost-migrations'.format(command.script) 9 | -------------------------------------------------------------------------------- /thefuck/rules/git_remote_delete.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return "remote delete" in command.script 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return re.sub(r"delete", "remove", command.script, 1) 14 | -------------------------------------------------------------------------------- /thefuck/rules/ls_lah.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('ls') 5 | def match(command): 6 | return command.script_parts and 'ls -' not in command.script 7 | 8 | 9 | def get_new_command(command): 10 | command = command.script_parts[:] 11 | command[0] = 'ls -lah' 12 | return ' '.join(command) 13 | -------------------------------------------------------------------------------- /tests/rules/test_sl_ls.py: -------------------------------------------------------------------------------- 1 | 2 | from thefuck.rules.sl_ls import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | assert match(Command('sl', '')) 8 | assert not match(Command('ls', '')) 9 | 10 | 11 | def test_get_new_command(): 12 | assert get_new_command(Command('sl', '')) == 'ls' 13 | -------------------------------------------------------------------------------- /thefuck/rules/mvn_no_command.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('mvn') 5 | def match(command): 6 | return 'No goals have been specified for this build' in command.output 7 | 8 | 9 | def get_new_command(command): 10 | return [command.script + ' clean package', 11 | command.script + ' clean install'] 12 | -------------------------------------------------------------------------------- /thefuck/rules/php_s.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument, for_app 2 | 3 | 4 | @for_app('php', at_least=2) 5 | def match(command): 6 | return ('-s' in command.script_parts 7 | and command.script_parts[-1] != '-s') 8 | 9 | 10 | def get_new_command(command): 11 | return replace_argument(command.script, "-s", "-S") 12 | -------------------------------------------------------------------------------- /thefuck/rules/yarn_command_replaced.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | regex = re.compile(r'Run "(.*)" instead') 5 | 6 | 7 | @for_app('yarn', at_least=1) 8 | def match(command): 9 | return regex.findall(command.output) 10 | 11 | 12 | def get_new_command(command): 13 | return regex.findall(command.output)[0] 14 | -------------------------------------------------------------------------------- /tests/rules/test_cd_parent.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.cd_parent import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('cd..', 'cd..: command not found')) 7 | assert not match(Command('', '')) 8 | 9 | 10 | def test_get_new_command(): 11 | assert get_new_command(Command('cd..', '')) == 'cd ..' 12 | -------------------------------------------------------------------------------- /thefuck/rules/cat_dir.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('cat', at_least=1) 6 | def match(command): 7 | return ( 8 | command.output.startswith('cat: ') and 9 | os.path.isdir(command.script_parts[1]) 10 | ) 11 | 12 | 13 | def get_new_command(command): 14 | return command.script.replace('cat', 'ls', 1) 15 | -------------------------------------------------------------------------------- /thefuck/rules/git_clone_git_clone.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return (' git clone ' in command.script 7 | and 'fatal: Too many arguments.' in command.output) 8 | 9 | 10 | @git_support 11 | def get_new_command(command): 12 | return command.script.replace(' git clone ', ' ', 1) 13 | -------------------------------------------------------------------------------- /thefuck/rules/tsuru_login.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('tsuru') 6 | def match(command): 7 | return ('not authenticated' in command.output 8 | and 'session has expired' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | return shell.and_('tsuru login', command.script) 13 | -------------------------------------------------------------------------------- /thefuck/rules/mkdir_p.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | @sudo_support 6 | def match(command): 7 | return ('mkdir' in command.script 8 | and 'No such file or directory' in command.output) 9 | 10 | 11 | @sudo_support 12 | def get_new_command(command): 13 | return re.sub('\\bmkdir (.*)', 'mkdir -p \\1', command.script) 14 | -------------------------------------------------------------------------------- /thefuck/rules/git_merge_unrelated.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return ('merge' in command.script 7 | and 'fatal: refusing to merge unrelated histories' in command.output) 8 | 9 | 10 | @git_support 11 | def get_new_command(command): 12 | return command.script + ' --allow-unrelated-histories' 13 | -------------------------------------------------------------------------------- /tests/rules/test_man_no_space.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.man_no_space import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('mandiff', 'mandiff: command not found')) 7 | assert not match(Command('', '')) 8 | 9 | 10 | def test_get_new_command(): 11 | assert get_new_command(Command('mandiff', '')) == 'man diff' 12 | -------------------------------------------------------------------------------- /thefuck/rules/java.py: -------------------------------------------------------------------------------- 1 | """Fixes common java command mistake 2 | 3 | Example: 4 | > java foo.java 5 | Error: Could not find or load main class foo.java 6 | 7 | """ 8 | from thefuck.utils import for_app 9 | 10 | 11 | @for_app('java') 12 | def match(command): 13 | return command.script.endswith('.java') 14 | 15 | 16 | def get_new_command(command): 17 | return command.script[:-5] 18 | -------------------------------------------------------------------------------- /thefuck/rules/git_push_without_commits.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return bool(re.search(r"src refspec \w+ does not match any", command.output)) 9 | 10 | 11 | def get_new_command(command): 12 | return shell.and_('git commit -m "Initial commit"', command.script) 13 | -------------------------------------------------------------------------------- /thefuck/rules/git_rebase_no_changes.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return ( 7 | {'rebase', '--continue'}.issubset(command.script_parts) and 8 | 'No changes - did you forget to use \'git add\'?' in command.output 9 | ) 10 | 11 | 12 | def get_new_command(command): 13 | return 'git rebase --skip' 14 | -------------------------------------------------------------------------------- /thefuck/rules/has_exists_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | @sudo_support 6 | def match(command): 7 | return command.script_parts and os.path.exists(command.script_parts[0]) \ 8 | and 'command not found' in command.output 9 | 10 | 11 | @sudo_support 12 | def get_new_command(command): 13 | return u'./{}'.format(command.script) 14 | -------------------------------------------------------------------------------- /thefuck/rules/apt_get_search.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.apt import apt_available 3 | from thefuck.utils import for_app 4 | 5 | enabled_by_default = apt_available 6 | 7 | 8 | @for_app('apt-get') 9 | def match(command): 10 | return command.script.startswith('apt-get search') 11 | 12 | 13 | def get_new_command(command): 14 | return re.sub(r'^apt-get', 'apt-cache', command.script) 15 | -------------------------------------------------------------------------------- /thefuck/rules/fix_alt_space.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import re 4 | from thefuck.specific.sudo import sudo_support 5 | 6 | 7 | @sudo_support 8 | def match(command): 9 | return ('command not found' in command.output.lower() 10 | and u' ' in command.script) 11 | 12 | 13 | @sudo_support 14 | def get_new_command(command): 15 | return re.sub(u' ', ' ', command.script) 16 | -------------------------------------------------------------------------------- /thefuck/rules/git_help_aliased.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return 'help' in command.script and ' is aliased to ' in command.output 7 | 8 | 9 | @git_support 10 | def get_new_command(command): 11 | aliased = command.output.split('`', 2)[2].split("'", 1)[0].split(' ', 1)[0] 12 | return 'git help {}'.format(aliased) 13 | -------------------------------------------------------------------------------- /thefuck/rules/git_remote_seturl_add.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('set-url' in command.script 8 | and 'fatal: No such remote' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | return replace_argument(command.script, 'set-url', 'add') 13 | -------------------------------------------------------------------------------- /thefuck/rules/cpp11.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('g++', 'clang++') 5 | def match(command): 6 | return ('This file requires compiler and library support for the ' 7 | 'ISO C++ 2011 standard.' in command.output or 8 | '-Wc++11-extensions' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | return command.script + ' -std=c++11' 13 | -------------------------------------------------------------------------------- /thefuck/rules/git_diff_staged.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('diff' in command.script and 8 | '--staged' not in command.script) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return replace_argument(command.script, 'diff', 'diff --staged') 14 | -------------------------------------------------------------------------------- /tests/rules/test_cd_cs.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.cd_cs import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('cs', 'cs: command not found')) 7 | assert match(Command('cs /etc/', 'cs: command not found')) 8 | 9 | 10 | def test_get_new_command(): 11 | assert get_new_command(Command('cs /etc/', 'cs: command not found')) == 'cd /etc/' 12 | -------------------------------------------------------------------------------- /thefuck/rules/cd_parent.py: -------------------------------------------------------------------------------- 1 | # Adds the missing space between the cd command and the target directory 2 | # when trying to cd to the parent directory. 3 | # 4 | # Does not really save chars, but is fun :D 5 | # 6 | # Example: 7 | # > cd.. 8 | # cd..: command not found 9 | 10 | 11 | def match(command): 12 | return command.script == 'cd..' 13 | 14 | 15 | def get_new_command(command): 16 | return 'cd ..' 17 | -------------------------------------------------------------------------------- /thefuck/rules/git_tag_force.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('tag' in command.script_parts 8 | and 'already exists' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return replace_argument(command.script, 'tag', 'tag --force') 14 | -------------------------------------------------------------------------------- /thefuck/rules/gradle_wrapper.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.utils import for_app, which 3 | 4 | 5 | @for_app('gradle') 6 | def match(command): 7 | return (not which(command.script_parts[0]) 8 | and 'not found' in command.output 9 | and os.path.isfile('gradlew')) 10 | 11 | 12 | def get_new_command(command): 13 | return u'./gradlew {}'.format(' '.join(command.script_parts[1:])) 14 | -------------------------------------------------------------------------------- /thefuck/rules/heroku_multiple_apps.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('heroku') 6 | def match(command): 7 | return 'https://devcenter.heroku.com/articles/multiple-environments' in command.output 8 | 9 | 10 | def get_new_command(command): 11 | apps = re.findall('([^ ]*) \\([^)]*\\)', command.output) 12 | return [command.script + ' --app ' + app for app in apps] 13 | -------------------------------------------------------------------------------- /tests/rules/test_python_command.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.python_command import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('temp.py', 'Permission denied')) 7 | assert not match(Command('', '')) 8 | 9 | 10 | def test_get_new_command(): 11 | assert (get_new_command(Command('./test_sudo.py', '')) 12 | == 'python ./test_sudo.py') 13 | -------------------------------------------------------------------------------- /thefuck/rules/brew_update_formula.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('brew', at_least=2) 5 | def match(command): 6 | return ('update' in command.script 7 | and "Error: This command updates brew itself" in command.output 8 | and "Use `brew upgrade" in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | return command.script.replace('update', 'upgrade') 13 | -------------------------------------------------------------------------------- /thefuck/rules/docker_login.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | from thefuck.shells import shell 3 | 4 | 5 | @for_app('docker') 6 | def match(command): 7 | return ('docker' in command.script 8 | and "access denied" in command.output 9 | and "may require 'docker login'" in command.output) 10 | 11 | 12 | def get_new_command(command): 13 | return shell.and_('docker login', command.script) 14 | -------------------------------------------------------------------------------- /thefuck/rules/git_branch_delete.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('branch -d' in command.script 8 | and 'If you are sure you want to delete it' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return replace_argument(command.script, '-d', '-D') 14 | -------------------------------------------------------------------------------- /thefuck/rules/python_execute.py: -------------------------------------------------------------------------------- 1 | # Appends .py when executing python files 2 | # 3 | # Example: 4 | # > python foo 5 | # error: python: can't open file 'foo': [Errno 2] No such file or directory 6 | from thefuck.utils import for_app 7 | 8 | 9 | @for_app('python') 10 | def match(command): 11 | return not command.script.endswith('.py') 12 | 13 | 14 | def get_new_command(command): 15 | return command.script + '.py' 16 | -------------------------------------------------------------------------------- /thefuck/specific/brew.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from ..utils import memoize, which 3 | 4 | 5 | brew_available = bool(which('brew')) 6 | 7 | 8 | @memoize 9 | def get_brew_path_prefix(): 10 | """To get brew path""" 11 | try: 12 | return subprocess.check_output(['brew', '--prefix'], 13 | universal_newlines=True).strip() 14 | except Exception: 15 | return None 16 | -------------------------------------------------------------------------------- /tests/rules/test_ls_all.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.ls_all import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('ls', '')) 7 | assert not match(Command('ls', 'file.py\n')) 8 | 9 | 10 | def test_get_new_command(): 11 | assert get_new_command(Command('ls empty_dir', '')) == 'ls -A empty_dir' 12 | assert get_new_command(Command('ls', '')) == 'ls -A' 13 | -------------------------------------------------------------------------------- /thefuck/rules/git_add_force.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('add' in command.script_parts 8 | and 'Use -f if you really want to add them.' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return replace_argument(command.script, 'add', 'add --force') 14 | -------------------------------------------------------------------------------- /thefuck/rules/go_run.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | # Appends .go when compiling go files 3 | # 4 | # Example: 5 | # > go run foo 6 | # error: go run: no go files listed 7 | 8 | 9 | @for_app('go') 10 | def match(command): 11 | return (command.script.startswith('go run ') 12 | and not command.script.endswith('.go')) 13 | 14 | 15 | def get_new_command(command): 16 | return command.script + '.go' 17 | -------------------------------------------------------------------------------- /thefuck/rules/git_push_different_branch_names.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return "push" in command.script and "The upstream branch of your current branch does not match" in command.output 8 | 9 | 10 | @git_support 11 | def get_new_command(command): 12 | return re.findall(r'^ +(git push [^\s]+ [^\s]+)', command.output, re.MULTILINE)[0] 13 | -------------------------------------------------------------------------------- /thefuck/rules/terraform_init.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('terraform') 6 | def match(command): 7 | return ('this module is not yet installed' in command.output.lower() or 8 | 'initialization required' in command.output.lower() 9 | ) 10 | 11 | 12 | def get_new_command(command): 13 | return shell.and_('terraform init', command.script) 14 | -------------------------------------------------------------------------------- /thefuck/rules/touch.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | from thefuck.utils import for_app 4 | 5 | 6 | @for_app('touch') 7 | def match(command): 8 | return 'No such file or directory' in command.output 9 | 10 | 11 | def get_new_command(command): 12 | path = re.findall( 13 | r"touch: (?:cannot touch ')?(.+)/.+'?:", command.output)[0] 14 | return shell.and_(u'mkdir -p {}'.format(path), command.script) 15 | -------------------------------------------------------------------------------- /thefuck/rules/cp_omitting_directory.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.utils import for_app 4 | 5 | 6 | @sudo_support 7 | @for_app('cp') 8 | def match(command): 9 | output = command.output.lower() 10 | return 'omitting directory' in output or 'is a directory' in output 11 | 12 | 13 | @sudo_support 14 | def get_new_command(command): 15 | return re.sub(r'^cp', 'cp -a', command.script) 16 | -------------------------------------------------------------------------------- /thefuck/rules/unsudo.py: -------------------------------------------------------------------------------- 1 | patterns = ['you cannot perform this operation as root'] 2 | 3 | 4 | def match(command): 5 | if command.script_parts and command.script_parts[0] != 'sudo': 6 | return False 7 | 8 | for pattern in patterns: 9 | if pattern in command.output.lower(): 10 | return True 11 | return False 12 | 13 | 14 | def get_new_command(command): 15 | return ' '.join(command.script_parts[1:]) 16 | -------------------------------------------------------------------------------- /thefuck/rules/apt_list_upgradable.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.apt import apt_available 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.utils import for_app 4 | 5 | enabled_by_default = apt_available 6 | 7 | 8 | @sudo_support 9 | @for_app('apt') 10 | def match(command): 11 | return 'apt list --upgradable' in command.output 12 | 13 | 14 | @sudo_support 15 | def get_new_command(command): 16 | return 'apt list --upgradable' 17 | -------------------------------------------------------------------------------- /thefuck/rules/yarn_alias.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_argument, for_app 3 | 4 | 5 | @for_app('yarn', at_least=1) 6 | def match(command): 7 | return 'Did you mean' in command.output 8 | 9 | 10 | def get_new_command(command): 11 | broken = command.script_parts[1] 12 | fix = re.findall(r'Did you mean [`"](?:yarn )?([^`"]*)[`"]', command.output)[0] 13 | 14 | return replace_argument(command.script, broken, fix) 15 | -------------------------------------------------------------------------------- /thefuck/rules/git_main_master.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return "'master'" in command.output or "'main'" in command.output 7 | 8 | 9 | @git_support 10 | def get_new_command(command): 11 | if "'master'" in command.output: 12 | return command.script.replace("master", "main") 13 | return command.script.replace("main", "master") 14 | 15 | 16 | priority = 1200 17 | -------------------------------------------------------------------------------- /thefuck/rules/javac.py: -------------------------------------------------------------------------------- 1 | """Appends .java when compiling java files 2 | 3 | Example: 4 | > javac foo 5 | error: Class names, 'foo', are only accepted if annotation 6 | processing is explicitly requested 7 | 8 | """ 9 | from thefuck.utils import for_app 10 | 11 | 12 | @for_app('javac') 13 | def match(command): 14 | return not command.script.endswith('.java') 15 | 16 | 17 | def get_new_command(command): 18 | return command.script + '.java' 19 | -------------------------------------------------------------------------------- /thefuck/rules/git_two_dashes.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('error: did you mean `' in command.output 8 | and '` (with two dashes ?)' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | to = command.output.split('`')[1] 14 | return replace_argument(command.script, to[1:], to) 15 | -------------------------------------------------------------------------------- /thefuck/rules/git_branch_list.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | # catches "git branch list" in place of "git branch" 8 | return (command.script_parts 9 | and command.script_parts[1:] == 'branch list'.split()) 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | return shell.and_('git branch --delete list', 'git branch') 15 | -------------------------------------------------------------------------------- /thefuck/rules/python_module_error.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | 4 | MISSING_MODULE = r"ModuleNotFoundError: No module named '([^']+)'" 5 | 6 | 7 | def match(command): 8 | return "ModuleNotFoundError: No module named '" in command.output 9 | 10 | 11 | def get_new_command(command): 12 | missing_module = re.findall(MISSING_MODULE, command.output)[0] 13 | return shell.and_("pip install {}".format(missing_module), command.script) 14 | -------------------------------------------------------------------------------- /thefuck/rules/rails_migrations_pending.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | 4 | 5 | SUGGESTION_REGEX = r"To resolve this issue, run:\s+(.*?)\n" 6 | 7 | 8 | def match(command): 9 | return "Migrations are pending. To resolve this issue, run:" in command.output 10 | 11 | 12 | def get_new_command(command): 13 | migration_script = re.search(SUGGESTION_REGEX, command.output).group(1) 14 | return shell.and_(migration_script, command.script) 15 | -------------------------------------------------------------------------------- /thefuck/rules/nixos_cmd_not_found.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.nix import nix_available 3 | from thefuck.shells import shell 4 | 5 | regex = re.compile(r'nix-env -iA ([^\s]*)') 6 | enabled_by_default = nix_available 7 | 8 | 9 | def match(command): 10 | return regex.findall(command.output) 11 | 12 | 13 | def get_new_command(command): 14 | name = regex.findall(command.output)[0] 15 | return shell.and_('nix-env -iA {}'.format(name), command.script) 16 | -------------------------------------------------------------------------------- /thefuck/rules/apt_upgrade.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.apt import apt_available 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.utils import for_app 4 | 5 | enabled_by_default = apt_available 6 | 7 | 8 | @sudo_support 9 | @for_app('apt') 10 | def match(command): 11 | return command.script == "apt list --upgradable" and len(command.output.strip().split('\n')) > 1 12 | 13 | 14 | @sudo_support 15 | def get_new_command(command): 16 | return 'apt upgrade' 17 | -------------------------------------------------------------------------------- /thefuck/rules/git_pull_clone.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('fatal: Not a git repository' in command.output 8 | and "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)." in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | return replace_argument(command.script, 'pull', 'clone') 14 | -------------------------------------------------------------------------------- /thefuck/rules/rm_dir.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | @sudo_support 6 | def match(command): 7 | return ('rm' in command.script 8 | and 'is a directory' in command.output.lower()) 9 | 10 | 11 | @sudo_support 12 | def get_new_command(command): 13 | arguments = '-rf' 14 | if 'hdfs' in command.script: 15 | arguments = '-r' 16 | return re.sub('\\brm (.*)', 'rm ' + arguments + ' \\1', command.script) 17 | -------------------------------------------------------------------------------- /thefuck/rules/brew_uninstall.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('brew', at_least=2) 5 | def match(command): 6 | return (command.script_parts[1] in ['uninstall', 'rm', 'remove'] 7 | and "brew uninstall --force" in command.output) 8 | 9 | 10 | def get_new_command(command): 11 | command_parts = command.script_parts[:] 12 | command_parts[1] = 'uninstall' 13 | command_parts.insert(2, '--force') 14 | return ' '.join(command_parts) 15 | -------------------------------------------------------------------------------- /thefuck/rules/history.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import get_close_matches, get_closest, \ 2 | get_valid_history_without_current 3 | 4 | 5 | def match(command): 6 | return len(get_close_matches(command.script, 7 | get_valid_history_without_current(command))) 8 | 9 | 10 | def get_new_command(command): 11 | return get_closest(command.script, 12 | get_valid_history_without_current(command)) 13 | 14 | 15 | priority = 9999 16 | -------------------------------------------------------------------------------- /thefuck/rules/git_pull_uncommitted_changes.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('pull' in command.script 8 | and ('You have unstaged changes' in command.output 9 | or 'contains uncommitted changes' in command.output)) 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | return shell.and_('git stash', 'git pull', 'git stash pop') 15 | -------------------------------------------------------------------------------- /thefuck/rules/conda_mistype.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_command, for_app 3 | 4 | 5 | @for_app("conda") 6 | def match(command): 7 | """ 8 | Match a mistyped command 9 | """ 10 | return "Did you mean 'conda" in command.output 11 | 12 | 13 | def get_new_command(command): 14 | match = re.findall(r"'conda ([^']*)'", command.output) 15 | broken_cmd = match[0] 16 | correct_cmd = match[1] 17 | return replace_command(command, broken_cmd, [correct_cmd]) 18 | -------------------------------------------------------------------------------- /thefuck/rules/git_stash.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | # catches "Please commit or stash them" and "Please, commit your changes or 8 | # stash them before you can switch branches." 9 | return 'or stash them' in command.output 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | formatme = shell.and_('git stash', '{}') 15 | return formatme.format(command.script) 16 | -------------------------------------------------------------------------------- /tests/test_readme.py: -------------------------------------------------------------------------------- 1 | def test_readme(source_root): 2 | with source_root.joinpath('README.md').open() as f: 3 | readme = f.read() 4 | 5 | bundled = source_root.joinpath('thefuck') \ 6 | .joinpath('rules') \ 7 | .glob('*.py') 8 | 9 | for rule in bundled: 10 | if rule.stem != '__init__': 11 | assert rule.stem in readme, \ 12 | 'Missing rule "{}" in README.md'.format(rule.stem) 13 | -------------------------------------------------------------------------------- /thefuck/rules/cargo_no_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_argument, for_app 3 | 4 | 5 | @for_app('cargo', at_least=1) 6 | def match(command): 7 | return ('no such subcommand' in command.output.lower() 8 | and 'Did you mean' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | broken = command.script_parts[1] 13 | fix = re.findall(r'Did you mean `([^`]*)`', command.output)[0] 14 | 15 | return replace_argument(command.script, broken, fix) 16 | -------------------------------------------------------------------------------- /thefuck/rules/dry.py: -------------------------------------------------------------------------------- 1 | def match(command): 2 | split_command = command.script_parts 3 | 4 | return (split_command 5 | and len(split_command) >= 2 6 | and split_command[0] == split_command[1]) 7 | 8 | 9 | def get_new_command(command): 10 | return ' '.join(command.script_parts[1:]) 11 | 12 | 13 | # it should be rare enough to actually have to type twice the same word, so 14 | # this rule can have a higher priority to come before things like "cd cd foo" 15 | priority = 900 16 | -------------------------------------------------------------------------------- /thefuck/rules/rm_root.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.sudo import sudo_support 2 | 3 | enabled_by_default = False 4 | 5 | 6 | @sudo_support 7 | def match(command): 8 | return (command.script_parts 9 | and {'rm', '/'}.issubset(command.script_parts) 10 | and '--no-preserve-root' not in command.script 11 | and '--no-preserve-root' in command.output) 12 | 13 | 14 | @sudo_support 15 | def get_new_command(command): 16 | return u'{} --no-preserve-root'.format(command.script) 17 | -------------------------------------------------------------------------------- /thefuck/rules/chmod_x.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.shells import shell 3 | 4 | 5 | def match(command): 6 | return (command.script.startswith('./') 7 | and 'permission denied' in command.output.lower() 8 | and os.path.exists(command.script_parts[0]) 9 | and not os.access(command.script_parts[0], os.X_OK)) 10 | 11 | 12 | def get_new_command(command): 13 | return shell.and_( 14 | 'chmod +x {}'.format(command.script_parts[0][2:]), 15 | command.script) 16 | -------------------------------------------------------------------------------- /thefuck/rules/cp_create_destination.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app("cp", "mv") 6 | def match(command): 7 | return ( 8 | "No such file or directory" in command.output 9 | or command.output.startswith("cp: directory") 10 | and command.output.rstrip().endswith("does not exist") 11 | ) 12 | 13 | 14 | def get_new_command(command): 15 | return shell.and_(u"mkdir -p {}".format(command.script_parts[-1]), command.script) 16 | -------------------------------------------------------------------------------- /thefuck/rules/git_commit_add.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import eager, replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ( 8 | "commit" in command.script_parts 9 | and "no changes added to commit" in command.output 10 | ) 11 | 12 | 13 | @eager 14 | @git_support 15 | def get_new_command(command): 16 | for opt in ("-a", "-p"): 17 | yield replace_argument(command.script, "commit", "commit {}".format(opt)) 18 | -------------------------------------------------------------------------------- /thefuck/rules/brew_link.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('brew', at_least=2) 5 | def match(command): 6 | return (command.script_parts[1] in ['ln', 'link'] 7 | and "brew link --overwrite --dry-run" in command.output) 8 | 9 | 10 | def get_new_command(command): 11 | command_parts = command.script_parts[:] 12 | command_parts[1] = 'link' 13 | command_parts.insert(2, '--overwrite') 14 | command_parts.insert(3, '--dry-run') 15 | return ' '.join(command_parts) 16 | -------------------------------------------------------------------------------- /thefuck/rules/cd_cs.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | # Redirects cs to cd when there is a typo 4 | # Due to the proximity of the keys - d and s - this seems like a common typo 5 | # ~ > cs /etc/ 6 | # cs: command not found 7 | # ~ > fuck 8 | # cd /etc/ [enter/↑/↓/ctrl+c] 9 | # /etc > 10 | 11 | 12 | def match(command): 13 | if command.script_parts[0] == 'cs': 14 | return True 15 | 16 | 17 | def get_new_command(command): 18 | return 'cd' + ''.join(command.script[2:]) 19 | 20 | 21 | priority = 900 22 | -------------------------------------------------------------------------------- /thefuck/rules/yarn_help.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | from thefuck.system import open_command 4 | 5 | 6 | @for_app('yarn', at_least=2) 7 | def match(command): 8 | return (command.script_parts[1] == 'help' 9 | and 'for documentation about this command.' in command.output) 10 | 11 | 12 | def get_new_command(command): 13 | url = re.findall( 14 | r'Visit ([^ ]*) for documentation about this command.', 15 | command.output)[0] 16 | 17 | return open_command(url) 18 | -------------------------------------------------------------------------------- /thefuck/rules/sed_unterminated_s.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | from thefuck.shells import shell 3 | from thefuck.utils import for_app 4 | 5 | 6 | @for_app('sed') 7 | def match(command): 8 | return "unterminated `s' command" in command.output 9 | 10 | 11 | def get_new_command(command): 12 | script = shlex.split(command.script) 13 | 14 | for (i, e) in enumerate(script): 15 | if e.startswith(('s/', '-es/')) and e[-1] != '/': 16 | script[i] += '/' 17 | 18 | return ' '.join(map(shell.quote, script)) 19 | -------------------------------------------------------------------------------- /thefuck/rules/git_rm_recursive.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return (' rm ' in command.script 7 | and "fatal: not removing '" in command.output 8 | and "' recursively without -r" in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | command_parts = command.script_parts[:] 14 | index = command_parts.index('rm') + 1 15 | command_parts.insert(index, '-r') 16 | return u' '.join(command_parts) 17 | -------------------------------------------------------------------------------- /thefuck/rules/pacman.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.archlinux import get_pkgfile, archlinux_env 2 | from thefuck.shells import shell 3 | 4 | 5 | def match(command): 6 | return 'not found' in command.output and get_pkgfile(command.script) 7 | 8 | 9 | def get_new_command(command): 10 | packages = get_pkgfile(command.script) 11 | 12 | formatme = shell.and_('{} -S {}', '{}') 13 | return [formatme.format(pacman, package, command.script) 14 | for package in packages] 15 | 16 | 17 | enabled_by_default, pacman = archlinux_env() 18 | -------------------------------------------------------------------------------- /thefuck/rules/unknown_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_command 3 | 4 | 5 | def match(command): 6 | return (re.search(r"([^:]*): Unknown command.*", command.output) is not None 7 | and re.search(r"Did you mean ([^?]*)?", command.output) is not None) 8 | 9 | 10 | def get_new_command(command): 11 | broken_cmd = re.findall(r"([^:]*): Unknown command.*", command.output)[0] 12 | matched = re.findall(r"Did you mean ([^?]*)?", command.output) 13 | return replace_command(command, broken_cmd, matched) 14 | -------------------------------------------------------------------------------- /thefuck/rules/git_diff_no_index.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | files = [arg for arg in command.script_parts[2:] 8 | if not arg.startswith('-')] 9 | return ('diff' in command.script 10 | and '--no-index' not in command.script 11 | and len(files) == 2) 12 | 13 | 14 | @git_support 15 | def get_new_command(command): 16 | return replace_argument(command.script, 'diff', 'diff --no-index') 17 | -------------------------------------------------------------------------------- /thefuck/rules/git_pull.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return 'pull' in command.script and 'set-upstream' in command.output 8 | 9 | 10 | @git_support 11 | def get_new_command(command): 12 | line = command.output.split('\n')[-3].strip() 13 | branch = line.split(' ')[-1] 14 | set_upstream = line.replace('', 'origin')\ 15 | .replace('', branch) 16 | return shell.and_(set_upstream, command.script) 17 | -------------------------------------------------------------------------------- /tests/rules/test_git_tag_force.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_tag_force import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''fatal: tag 'alert' already exists''' 9 | 10 | 11 | def test_match(output): 12 | assert match(Command('git tag alert', output)) 13 | assert not match(Command('git tag alert', '')) 14 | 15 | 16 | def test_get_new_command(output): 17 | assert (get_new_command(Command('git tag alert', output)) 18 | == "git tag --force alert") 19 | -------------------------------------------------------------------------------- /thefuck/rules/terraform_no_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | MISTAKE = r'(?<=Terraform has no command named ")([^"]+)(?="\.)' 5 | FIX = r'(?<=Did you mean ")([^"]+)(?="\?)' 6 | 7 | 8 | @for_app('terraform') 9 | def match(command): 10 | return re.search(MISTAKE, command.output) and re.search(FIX, command.output) 11 | 12 | 13 | def get_new_command(command): 14 | mistake = re.search(MISTAKE, command.output).group(0) 15 | fix = re.search(FIX, command.output).group(0) 16 | return command.script.replace(mistake, fix) 17 | -------------------------------------------------------------------------------- /tests/test_logs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck import logs 3 | 4 | 5 | def test_color(settings): 6 | settings.no_colors = False 7 | assert logs.color('red') == 'red' 8 | settings.no_colors = True 9 | assert logs.color('red') == '' 10 | 11 | 12 | @pytest.mark.usefixtures('no_colors') 13 | @pytest.mark.parametrize('debug, stderr', [ 14 | (True, 'DEBUG: test\n'), 15 | (False, '')]) 16 | def test_debug(capsys, settings, debug, stderr): 17 | settings.debug = debug 18 | logs.debug('test') 19 | assert capsys.readouterr() == ('', stderr) 20 | -------------------------------------------------------------------------------- /thefuck/rules/tmux.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_command, for_app 3 | 4 | 5 | @for_app('tmux') 6 | def match(command): 7 | return ('ambiguous command:' in command.output 8 | and 'could be:' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | cmd = re.match(r"ambiguous command: (.*), could be: (.*)", 13 | command.output) 14 | 15 | old_cmd = cmd.group(1) 16 | suggestions = [c.strip() for c in cmd.group(2).split(',')] 17 | 18 | return replace_command(command, old_cmd, suggestions) 19 | -------------------------------------------------------------------------------- /thefuck/rules/git_bisect_usage.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_command 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return ('bisect' in command.script_parts and 9 | 'usage: git bisect' in command.output) 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | broken = re.findall(r'git bisect ([^ $]*).*', command.script)[0] 15 | usage = re.findall(r'usage: git bisect \[([^\]]+)\]', command.output)[0] 16 | return replace_command(command, broken, usage.split('|')) 17 | -------------------------------------------------------------------------------- /thefuck/rules/git_stash_pop.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('stash' in command.script 8 | and 'pop' in command.script 9 | and 'Your local changes to the following files would be overwritten by merge' in command.output) 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | return shell.and_('git add --update', 'git stash pop', 'git reset .') 15 | 16 | 17 | # make it come before the other applicable rules 18 | priority = 900 19 | -------------------------------------------------------------------------------- /thefuck/rules/npm_run_script.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.npm import npm_available, get_scripts 2 | from thefuck.utils import for_app 3 | 4 | enabled_by_default = npm_available 5 | 6 | 7 | @for_app('npm') 8 | def match(command): 9 | return ('Usage: npm ' in command.output 10 | and not any(part.startswith('ru') for part in command.script_parts) 11 | and command.script_parts[1] in get_scripts()) 12 | 13 | 14 | def get_new_command(command): 15 | parts = command.script_parts[:] 16 | parts.insert(1, 'run-script') 17 | return ' '.join(parts) 18 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.163.1/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 4 | ARG VARIANT="3" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 8 | COPY requirements.txt /tmp/pip-tmp/ 9 | RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 10 | && rm -rf /tmp/pip-tmp 11 | -------------------------------------------------------------------------------- /tests/rules/test_grep_recursive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from thefuck.rules.grep_recursive import match, get_new_command 4 | from thefuck.types import Command 5 | 6 | 7 | def test_match(): 8 | assert match(Command('grep blah .', 'grep: .: Is a directory')) 9 | assert match(Command(u'grep café .', 'grep: .: Is a directory')) 10 | assert not match(Command('', '')) 11 | 12 | 13 | def test_get_new_command(): 14 | assert get_new_command(Command('grep blah .', '')) == 'grep -r blah .' 15 | assert get_new_command(Command(u'grep café .', '')) == u'grep -r café .' 16 | -------------------------------------------------------------------------------- /thefuck/rules/python_command.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.sudo import sudo_support 2 | # add 'python' suffix to the command if 3 | # 1) The script does not have execute permission or 4 | # 2) is interpreted as shell script 5 | 6 | 7 | @sudo_support 8 | def match(command): 9 | return (command.script_parts 10 | and command.script_parts[0].endswith('.py') 11 | and ('Permission denied' in command.output or 12 | 'command not found' in command.output)) 13 | 14 | 15 | @sudo_support 16 | def get_new_command(command): 17 | return 'python ' + command.script 18 | -------------------------------------------------------------------------------- /thefuck/specific/sudo.py: -------------------------------------------------------------------------------- 1 | import six 2 | from decorator import decorator 3 | 4 | 5 | @decorator 6 | def sudo_support(fn, command): 7 | """Removes sudo before calling fn and adds it after.""" 8 | if not command.script.startswith('sudo '): 9 | return fn(command) 10 | 11 | result = fn(command.update(script=command.script[5:])) 12 | 13 | if result and isinstance(result, six.string_types): 14 | return u'sudo {}'.format(result) 15 | elif isinstance(result, list): 16 | return [u'sudo {}'.format(x) for x in result] 17 | else: 18 | return result 19 | -------------------------------------------------------------------------------- /tests/rules/test_go_run.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.go_run import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('go run foo', ''), 8 | Command('go run bar', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('go run foo', ''), 'go run foo.go'), 15 | (Command('go run bar', ''), 'go run bar.go')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /tests/rules/test_java.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.java import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('java foo.java', ''), 8 | Command('java bar.java', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('java foo.java', ''), 'java foo'), 15 | (Command('java bar.java', ''), 'java bar')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /tests/rules/test_javac.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.javac import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('javac foo', ''), 8 | Command('javac bar', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('javac foo', ''), 'javac foo.java'), 15 | (Command('javac bar', ''), 'javac bar.java')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /thefuck/rules/pip_install.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | @sudo_support 6 | @for_app('pip') 7 | def match(command): 8 | return ('pip install' in command.script and 'Permission denied' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | if '--user' not in command.script: # add --user (attempt 1) 13 | return command.script.replace(' install ', ' install --user ') 14 | 15 | return 'sudo {}'.format(command.script.replace(' --user', '')) # since --user didn't fix things, let's try sudo (attempt 2) 16 | -------------------------------------------------------------------------------- /thefuck/rules/wrong_hyphen_before_subcommand.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import get_all_executables 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | @sudo_support 6 | def match(command): 7 | first_part = command.script_parts[0] 8 | if "-" not in first_part or first_part in get_all_executables(): 9 | return False 10 | cmd, _ = first_part.split("-", 1) 11 | return cmd in get_all_executables() 12 | 13 | 14 | @sudo_support 15 | def get_new_command(command): 16 | return command.script.replace("-", " ", 1) 17 | 18 | 19 | priority = 4500 20 | requires_output = False 21 | -------------------------------------------------------------------------------- /thefuck/rules/aws_cli.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from thefuck.utils import for_app, replace_argument 4 | 5 | INVALID_CHOICE = "(?<=Invalid choice: ')(.*)(?=', maybe you meant:)" 6 | OPTIONS = "^\\s*\\*\\s(.*)" 7 | 8 | 9 | @for_app('aws') 10 | def match(command): 11 | return "usage:" in command.output and "maybe you meant:" in command.output 12 | 13 | 14 | def get_new_command(command): 15 | mistake = re.search(INVALID_CHOICE, command.output).group(0) 16 | options = re.findall(OPTIONS, command.output, flags=re.MULTILINE) 17 | return [replace_argument(command.script, mistake, o) for o in options] 18 | -------------------------------------------------------------------------------- /thefuck/rules/tsuru_not_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import get_all_matched_commands, replace_command, for_app 3 | 4 | 5 | @for_app('tsuru') 6 | def match(command): 7 | return (' is not a tsuru command. See "tsuru help".' in command.output 8 | and '\nDid you mean?\n\t' in command.output) 9 | 10 | 11 | def get_new_command(command): 12 | broken_cmd = re.findall(r'tsuru: "([^"]*)" is not a tsuru command', 13 | command.output)[0] 14 | return replace_command(command, broken_cmd, 15 | get_all_matched_commands(command.output)) 16 | -------------------------------------------------------------------------------- /tests/rules/test_python_execute.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.python_execute import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('python foo', ''), 8 | Command('python bar', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('python foo', ''), 'python foo.py'), 15 | (Command('python bar', ''), 'python bar.py')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /thefuck/rules/npm_missing_script.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app, replace_command 3 | from thefuck.specific.npm import get_scripts, npm_available 4 | 5 | enabled_by_default = npm_available 6 | 7 | 8 | @for_app('npm') 9 | def match(command): 10 | return (any(part.startswith('ru') for part in command.script_parts) 11 | and 'npm ERR! missing script: ' in command.output) 12 | 13 | 14 | def get_new_command(command): 15 | misspelled_script = re.findall( 16 | r'.*missing script: (.*)\n', command.output)[0] 17 | return replace_command(command, misspelled_script, get_scripts()) 18 | -------------------------------------------------------------------------------- /tests/rules/test_dry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.dry import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('cd cd foo', ''), 8 | Command('git git push origin/master', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('cd cd foo', ''), 'cd foo'), 15 | (Command('git git push origin/master', ''), 'git push origin/master')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /tests/rules/test_git_stash_pop.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_stash_pop import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''error: Your local changes to the following files would be overwritten by merge:''' 9 | 10 | 11 | def test_match(output): 12 | assert match(Command('git stash pop', output)) 13 | assert not match(Command('git stash', '')) 14 | 15 | 16 | def test_get_new_command(output): 17 | assert (get_new_command(Command('git stash pop', output)) 18 | == "git add --update && git stash pop && git reset .") 19 | -------------------------------------------------------------------------------- /tests/rules/test_ls_lah.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.ls_lah import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('ls', '')) 7 | assert match(Command('ls file.py', '')) 8 | assert match(Command('ls /opt', '')) 9 | assert not match(Command('ls -lah /opt', '')) 10 | assert not match(Command('pacman -S binutils', '')) 11 | assert not match(Command('lsof', '')) 12 | 13 | 14 | def test_get_new_command(): 15 | assert get_new_command(Command('ls file.py', '')) == 'ls -lah file.py' 16 | assert get_new_command(Command('ls', '')) == 'ls -lah' 17 | -------------------------------------------------------------------------------- /thefuck/output_readers/__init__.py: -------------------------------------------------------------------------------- 1 | from ..conf import settings 2 | from . import read_log, rerun, shell_logger 3 | 4 | 5 | def get_output(script, expanded): 6 | """Get output of the script. 7 | 8 | :param script: Console script. 9 | :type script: str 10 | :param expanded: Console script with expanded aliases. 11 | :type expanded: str 12 | :rtype: str 13 | 14 | """ 15 | if shell_logger.is_available(): 16 | return shell_logger.get_output(script) 17 | if settings.instant_mode: 18 | return read_log.get_output(script) 19 | else: 20 | return rerun.get_output(script, expanded) 21 | -------------------------------------------------------------------------------- /thefuck/rules/git_push_force.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return ('push' in command.script 8 | and '! [rejected]' in command.output 9 | and 'failed to push some refs to' in command.output 10 | and 'Updates were rejected because the tip of your current branch is behind' in command.output) 11 | 12 | 13 | @git_support 14 | def get_new_command(command): 15 | return replace_argument(command.script, 'push', 'push --force-with-lease') 16 | 17 | 18 | enabled_by_default = False 19 | -------------------------------------------------------------------------------- /thefuck/rules/ln_no_hard_link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Suggest creating symbolic link if hard link is not allowed. 3 | 4 | Example: 5 | > ln barDir barLink 6 | ln: ‘barDir’: hard link not allowed for directory 7 | 8 | --> ln -s barDir barLink 9 | """ 10 | 11 | import re 12 | from thefuck.specific.sudo import sudo_support 13 | 14 | 15 | @sudo_support 16 | def match(command): 17 | return (command.output.endswith("hard link not allowed for directory") and 18 | command.script_parts[0] == 'ln') 19 | 20 | 21 | @sudo_support 22 | def get_new_command(command): 23 | return re.sub(r'^ln ', 'ln -s ', command.script) 24 | -------------------------------------------------------------------------------- /thefuck/rules/git_branch_delete_checked_out.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | from thefuck.utils import replace_argument 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return ( 9 | ("branch -d" in command.script or "branch -D" in command.script) 10 | and "error: Cannot delete branch '" in command.output 11 | and "' checked out at '" in command.output 12 | ) 13 | 14 | 15 | @git_support 16 | def get_new_command(command): 17 | return shell.and_("git checkout master", "{}").format( 18 | replace_argument(command.script, "-d", "-D") 19 | ) 20 | -------------------------------------------------------------------------------- /thefuck/rules/az_cli.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from thefuck.utils import for_app, replace_argument 4 | 5 | INVALID_CHOICE = "(?=az)(?:.*): '(.*)' is not in the '.*' command group." 6 | OPTIONS = "^The most similar choice to '.*' is:\n\\s*(.*)$" 7 | 8 | 9 | @for_app('az') 10 | def match(command): 11 | return "is not in the" in command.output and "command group" in command.output 12 | 13 | 14 | def get_new_command(command): 15 | mistake = re.search(INVALID_CHOICE, command.output).group(1) 16 | options = re.findall(OPTIONS, command.output, flags=re.MULTILINE) 17 | return [replace_argument(command.script, mistake, o) for o in options] 18 | -------------------------------------------------------------------------------- /thefuck/rules/pacman_invalid_option.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.archlinux import archlinux_env 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.utils import for_app 4 | import re 5 | 6 | 7 | @sudo_support 8 | @for_app("pacman") 9 | def match(command): 10 | return command.output.startswith("error: invalid option '-") and any( 11 | " -{}".format(option) in command.script for option in "surqfdvt" 12 | ) 13 | 14 | 15 | def get_new_command(command): 16 | option = re.findall(r" -[dfqrstuv]", command.script)[0] 17 | return re.sub(option, option.upper(), command.script) 18 | 19 | 20 | enabled_by_default = archlinux_env() 21 | -------------------------------------------------------------------------------- /thefuck/rules/vagrant_up.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.utils import for_app 3 | 4 | 5 | @for_app('vagrant') 6 | def match(command): 7 | return 'run `vagrant up`' in command.output.lower() 8 | 9 | 10 | def get_new_command(command): 11 | cmds = command.script_parts 12 | machine = None 13 | if len(cmds) >= 3: 14 | machine = cmds[2] 15 | 16 | start_all_instances = shell.and_(u"vagrant up", command.script) 17 | if machine is None: 18 | return start_all_instances 19 | else: 20 | return [shell.and_(u"vagrant up {}".format(machine), command.script), 21 | start_all_instances] 22 | -------------------------------------------------------------------------------- /tests/functional/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_docker_pexpect.docker import run as pexpect_docker_run, \ 4 | stats as pexpect_docker_stats 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def build_container_mock(mocker): 9 | return mocker.patch('pytest_docker_pexpect.docker.build_container') 10 | 11 | 12 | def run_side_effect(*args, **kwargs): 13 | container_id = pexpect_docker_run(*args, **kwargs) 14 | pexpect_docker_stats(container_id) 15 | return container_id 16 | 17 | 18 | @pytest.fixture(autouse=True) 19 | def run_mock(mocker): 20 | return mocker.patch('pytest_docker_pexpect.docker.run', side_effect=run_side_effect) 21 | -------------------------------------------------------------------------------- /tests/rules/test_rm_root.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.rm_root import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | assert match(Command('rm -rf /', 'add --no-preserve-root')) 8 | 9 | 10 | @pytest.mark.parametrize('command', [ 11 | Command('ls', 'add --no-preserve-root'), 12 | Command('rm --no-preserve-root /', 'add --no-preserve-root'), 13 | Command('rm -rf /', '')]) 14 | def test_not_match(command): 15 | assert not match(command) 16 | 17 | 18 | def test_get_new_command(): 19 | assert (get_new_command(Command('rm -rf /', '')) 20 | == 'rm -rf / --no-preserve-root') 21 | -------------------------------------------------------------------------------- /thefuck/specific/npm.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import Popen, PIPE 3 | from thefuck.utils import memoize, eager, which 4 | 5 | npm_available = bool(which('npm')) 6 | 7 | 8 | @memoize 9 | @eager 10 | def get_scripts(): 11 | """Get custom npm scripts.""" 12 | proc = Popen(['npm', 'run-script'], stdout=PIPE) 13 | should_yeild = False 14 | for line in proc.stdout.readlines(): 15 | line = line.decode() 16 | if 'available via `npm run-script`:' in line: 17 | should_yeild = True 18 | continue 19 | 20 | if should_yeild and re.match(r'^ [^ ]+', line): 21 | yield line.strip().split(' ')[0] 22 | -------------------------------------------------------------------------------- /tests/rules/test_git_pull_uncommitted_changes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_pull_uncommitted_changes import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''error: Cannot pull with rebase: You have unstaged changes.''' 9 | 10 | 11 | def test_match(output): 12 | assert match(Command('git pull', output)) 13 | assert not match(Command('git pull', '')) 14 | assert not match(Command('ls', output)) 15 | 16 | 17 | def test_get_new_command(output): 18 | assert (get_new_command(Command('git pull', output)) 19 | == "git stash && git pull && git stash pop") 20 | -------------------------------------------------------------------------------- /thefuck/rules/git_lfs_mistype.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import get_all_matched_commands, replace_command 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | ''' 9 | Match a mistyped command 10 | ''' 11 | return 'lfs' in command.script and 'Did you mean this?' in command.output 12 | 13 | 14 | @git_support 15 | def get_new_command(command): 16 | broken_cmd = re.findall(r'Error: unknown command "([^"]*)" for "git-lfs"', command.output)[0] 17 | matched = get_all_matched_commands(command.output, ['Did you mean', ' for usage.']) 18 | return replace_command(command, broken_cmd, matched) 19 | -------------------------------------------------------------------------------- /tests/rules/test_lein_not_task.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.lein_not_task import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def is_not_task(): 8 | return ''''rpl' is not a task. See 'lein help'. 9 | 10 | Did you mean this? 11 | repl 12 | jar 13 | ''' 14 | 15 | 16 | def test_match(is_not_task): 17 | assert match(Command('lein rpl', is_not_task)) 18 | assert not match(Command('ls', is_not_task)) 19 | 20 | 21 | def test_get_new_command(is_not_task): 22 | assert (get_new_command(Command('lein rpl --help', is_not_task)) 23 | == ['lein repl --help', 'lein jar --help']) 24 | -------------------------------------------------------------------------------- /thefuck/rules/brew_reinstall.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | 5 | warning_regex = re.compile(r'Warning: (?:.(?!is ))+ is already installed and ' 6 | r'up-to-date') 7 | message_regex = re.compile(r'To reinstall (?:(?!, ).)+, run `brew reinstall ' 8 | r'[^`]+`') 9 | 10 | 11 | @for_app('brew', at_least=2) 12 | def match(command): 13 | return ('install' in command.script 14 | and warning_regex.search(command.output) 15 | and message_regex.search(command.output)) 16 | 17 | 18 | def get_new_command(command): 19 | return command.script.replace('install', 'reinstall') 20 | -------------------------------------------------------------------------------- /thefuck/rules/remove_shell_prompt_literal.py: -------------------------------------------------------------------------------- 1 | """Fixes error for commands containing one or more occurrences of the shell 2 | prompt symbol '$'. 3 | 4 | This usually happens when commands are copied from documentations 5 | including them in their code blocks. 6 | 7 | Example: 8 | > $ git clone https://github.com/nvbn/thefuck.git 9 | bash: $: command not found... 10 | """ 11 | 12 | import re 13 | 14 | 15 | def match(command): 16 | return ( 17 | "$: command not found" in command.output 18 | and re.search(r"^[\s]*\$ [\S]+", command.script) is not None 19 | ) 20 | 21 | 22 | def get_new_command(command): 23 | return command.script.lstrip("$ ") 24 | -------------------------------------------------------------------------------- /tests/rules/test_git_branch_list.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.git_branch_list import match, get_new_command 2 | from thefuck.shells import shell 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | assert match(Command('git branch list', '')) 8 | 9 | 10 | def test_not_match(): 11 | assert not match(Command('', '')) 12 | assert not match(Command('git commit', '')) 13 | assert not match(Command('git branch', '')) 14 | assert not match(Command('git stash list', '')) 15 | 16 | 17 | def test_get_new_command(): 18 | assert (get_new_command(Command('git branch list', '')) == 19 | shell.and_('git branch --delete list', 'git branch')) 20 | -------------------------------------------------------------------------------- /tests/rules/test_git_pull_unstaged_changes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_pull_uncommitted_changes import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''error: Cannot pull with rebase: Your index contains uncommitted changes.''' 9 | 10 | 11 | def test_match(output): 12 | assert match(Command('git pull', output)) 13 | assert not match(Command('git pull', '')) 14 | assert not match(Command('ls', output)) 15 | 16 | 17 | def test_get_new_command(output): 18 | assert (get_new_command(Command('git pull', output)) 19 | == "git stash && git pull && git stash pop") 20 | -------------------------------------------------------------------------------- /tests/rules/test_remove_trailing_cedilla.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.remove_trailing_cedilla import match, get_new_command, CEDILLA 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('wrong' + CEDILLA, ''), 8 | Command('wrong with args' + CEDILLA, '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command, new_command', [ 14 | (Command('wrong' + CEDILLA, ''), 'wrong'), 15 | (Command('wrong with args' + CEDILLA, ''), 'wrong with args')]) 16 | def test_get_new_command(command, new_command): 17 | assert get_new_command(command) == new_command 18 | -------------------------------------------------------------------------------- /tests/rules/test_tmux.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.tmux import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def tmux_ambiguous(): 8 | return "ambiguous command: list, could be: " \ 9 | "list-buffers, list-clients, list-commands, list-keys, " \ 10 | "list-panes, list-sessions, list-windows" 11 | 12 | 13 | def test_match(tmux_ambiguous): 14 | assert match(Command('tmux list', tmux_ambiguous)) 15 | 16 | 17 | def test_get_new_command(tmux_ambiguous): 18 | assert get_new_command(Command('tmux list', tmux_ambiguous))\ 19 | == ['tmux list-keys', 'tmux list-panes', 'tmux list-windows'] 20 | -------------------------------------------------------------------------------- /thefuck/rules/cd_mkdir.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | from thefuck.specific.sudo import sudo_support 4 | from thefuck.shells import shell 5 | 6 | 7 | @sudo_support 8 | @for_app('cd') 9 | def match(command): 10 | return ( 11 | command.script.startswith('cd ') and any(( 12 | 'no such file or directory' in command.output.lower(), 13 | 'cd: can\'t cd to' in command.output.lower(), 14 | 'does not exist' in command.output.lower() 15 | ))) 16 | 17 | 18 | @sudo_support 19 | def get_new_command(command): 20 | repl = shell.and_('mkdir -p \\1', 'cd \\1') 21 | return re.sub(r'^cd (.*)', repl, command.script) 22 | -------------------------------------------------------------------------------- /thefuck/rules/git_merge.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_argument 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return ('merge' in command.script 9 | and ' - not something we can merge' in command.output 10 | and 'Did you mean this?' in command.output) 11 | 12 | 13 | @git_support 14 | def get_new_command(command): 15 | unknown_branch = re.findall(r'merge: (.+) - not something we can merge', command.output)[0] 16 | remote_branch = re.findall(r'Did you mean this\?\n\t([^\n]+)', command.output)[0] 17 | 18 | return replace_argument(command.script, unknown_branch, remote_branch) 19 | -------------------------------------------------------------------------------- /thefuck/rules/missing_space_before_subcommand.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import get_all_executables, memoize 2 | 3 | 4 | @memoize 5 | def _get_executable(script_part): 6 | for executable in get_all_executables(): 7 | if len(executable) > 1 and script_part.startswith(executable): 8 | return executable 9 | 10 | 11 | def match(command): 12 | return (not command.script_parts[0] in get_all_executables() 13 | and _get_executable(command.script_parts[0])) 14 | 15 | 16 | def get_new_command(command): 17 | executable = _get_executable(command.script_parts[0]) 18 | return command.script.replace(executable, u'{} '.format(executable), 1) 19 | 20 | 21 | priority = 4000 22 | -------------------------------------------------------------------------------- /thefuck/rules/systemctl.py: -------------------------------------------------------------------------------- 1 | """ 2 | The confusion in systemctl's param order is massive. 3 | """ 4 | from thefuck.specific.sudo import sudo_support 5 | from thefuck.utils import for_app 6 | 7 | 8 | @sudo_support 9 | @for_app('systemctl') 10 | def match(command): 11 | # Catches "Unknown operation 'service'." when executing systemctl with 12 | # misordered arguments 13 | cmd = command.script_parts 14 | return (cmd and 'Unknown operation \'' in command.output and 15 | len(cmd) - cmd.index('systemctl') == 3) 16 | 17 | 18 | @sudo_support 19 | def get_new_command(command): 20 | cmd = command.script_parts[:] 21 | cmd[-1], cmd[-2] = cmd[-2], cmd[-1] 22 | return ' '.join(cmd) 23 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: thefuck 2 | version: stable 3 | version-script: git -C parts/thefuck/build describe --abbrev=0 --tags 4 | summary: Magnificent app which corrects your previous console command. 5 | description: | 6 | The Fuck tries to match a rule for the previous command, 7 | creates a new command using the matched rule and runs it. 8 | 9 | grade: stable 10 | confinement: classic 11 | 12 | apps: 13 | thefuck: 14 | command: bin/thefuck 15 | environment: 16 | PYTHONIOENCODING: utf-8 17 | fuck: 18 | command: bin/fuck 19 | environment: 20 | PYTHONIOENCODING: utf-8 21 | 22 | parts: 23 | thefuck: 24 | source: https://github.com/nvbn/thefuck.git 25 | plugin: python 26 | -------------------------------------------------------------------------------- /thefuck/rules/pip_unknown_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_argument, for_app 3 | from thefuck.specific.sudo import sudo_support 4 | 5 | 6 | @sudo_support 7 | @for_app('pip', 'pip2', 'pip3') 8 | def match(command): 9 | return ('pip' in command.script and 10 | 'unknown command' in command.output and 11 | 'maybe you meant' in command.output) 12 | 13 | 14 | def get_new_command(command): 15 | broken_cmd = re.findall(r'ERROR: unknown command "([^"]+)"', 16 | command.output)[0] 17 | new_cmd = re.findall(r'maybe you meant "([^"]+)"', command.output)[0] 18 | 19 | return replace_argument(command.script, broken_cmd, new_cmd) 20 | -------------------------------------------------------------------------------- /tests/rules/test_conda_mistype.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thefuck.rules.conda_mistype import match, get_new_command 4 | from thefuck.types import Command 5 | 6 | 7 | @pytest.fixture 8 | def mistype_response(): 9 | return """ 10 | 11 | CommandNotFoundError: No command 'conda lst'. 12 | Did you mean 'conda list'? 13 | 14 | """ 15 | 16 | 17 | def test_match(mistype_response): 18 | assert match(Command('conda lst', mistype_response)) 19 | err_response = 'bash: codna: command not found' 20 | assert not match(Command('codna list', err_response)) 21 | 22 | 23 | def test_get_new_command(mistype_response): 24 | assert (get_new_command(Command('conda lst', mistype_response)) == ['conda list']) 25 | -------------------------------------------------------------------------------- /thefuck/rules/sudo_command_from_user_path.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app, which, replace_argument 3 | 4 | 5 | def _get_command_name(command): 6 | found = re.findall(r'sudo: (.*): command not found', command.output) 7 | if found: 8 | return found[0] 9 | 10 | 11 | @for_app('sudo') 12 | def match(command): 13 | if 'command not found' in command.output: 14 | command_name = _get_command_name(command) 15 | return which(command_name) 16 | 17 | 18 | def get_new_command(command): 19 | command_name = _get_command_name(command) 20 | return replace_argument(command.script, command_name, 21 | u'env "PATH=$PATH" {}'.format(command_name)) 22 | -------------------------------------------------------------------------------- /scripts/fuck.ps1: -------------------------------------------------------------------------------- 1 | if ((Get-Command "fuck").CommandType -eq "Function") { 2 | fuck @args; 3 | [Console]::ResetColor() 4 | exit 5 | } 6 | 7 | "First time use of thefuck detected. " 8 | 9 | if ((Get-Content $PROFILE -Raw -ErrorAction Ignore) -like "*thefuck*") { 10 | } else { 11 | " - Adding thefuck intialization to user `$PROFILE" 12 | $script = "`n`$env:PYTHONIOENCODING='utf-8' `niex `"`$(thefuck --alias)`""; 13 | Write-Output $script | Add-Content $PROFILE 14 | } 15 | 16 | " - Adding fuck() function to current session..." 17 | $env:PYTHONIOENCODING='utf-8' 18 | iex "$($(thefuck --alias).Replace("function fuck", "function global:fuck"))" 19 | 20 | " - Invoking fuck()`n" 21 | fuck @args; 22 | [Console]::ResetColor() 23 | -------------------------------------------------------------------------------- /thefuck/rules/git_rebase_merge_dir.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import get_close_matches 2 | from thefuck.specific.git import git_support 3 | 4 | 5 | @git_support 6 | def match(command): 7 | return (' rebase' in command.script and 8 | 'It seems that there is already a rebase-merge directory' in command.output and 9 | 'I wonder if you are in the middle of another rebase' in command.output) 10 | 11 | 12 | @git_support 13 | def get_new_command(command): 14 | command_list = ['git rebase --continue', 'git rebase --abort', 'git rebase --skip'] 15 | rm_cmd = command.output.split('\n')[-4] 16 | command_list.append(rm_cmd.strip()) 17 | return get_close_matches(command.script, command_list, 4, 0) 18 | -------------------------------------------------------------------------------- /thefuck/rules/grep_arguments_order.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.utils import for_app 3 | 4 | 5 | def _get_actual_file(parts): 6 | for part in parts[1:]: 7 | if os.path.isfile(part) or os.path.isdir(part): 8 | return part 9 | 10 | 11 | @for_app('grep', 'egrep') 12 | def match(command): 13 | return ': No such file or directory' in command.output \ 14 | and _get_actual_file(command.script_parts) 15 | 16 | 17 | def get_new_command(command): 18 | actual_file = _get_actual_file(command.script_parts) 19 | parts = command.script_parts[::] 20 | # Moves file to the end of the script: 21 | parts.remove(actual_file) 22 | parts.append(actual_file) 23 | return ' '.join(parts) 24 | -------------------------------------------------------------------------------- /tests/rules/test_git_branch_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_branch_delete import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''error: The branch 'branch' is not fully merged. 9 | If you are sure you want to delete it, run 'git branch -D branch'. 10 | 11 | ''' 12 | 13 | 14 | def test_match(output): 15 | assert match(Command('git branch -d branch', output)) 16 | assert not match(Command('git branch -d branch', '')) 17 | assert not match(Command('ls', output)) 18 | 19 | 20 | def test_get_new_command(output): 21 | assert get_new_command(Command('git branch -d branch', output))\ 22 | == "git branch -D branch" 23 | -------------------------------------------------------------------------------- /tests/rules/test_long_form_help.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.long_form_help import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('output', [ 7 | 'Try \'grep --help\' for more information.']) 8 | def test_match(output): 9 | assert match(Command('grep -h', output)) 10 | 11 | 12 | def test_not_match(): 13 | assert not match(Command('', '')) 14 | 15 | 16 | @pytest.mark.parametrize('before, after', [ 17 | ('grep -h', 'grep --help'), 18 | ('tar -h', 'tar --help'), 19 | ('docker run -h', 'docker run --help'), 20 | ('cut -h', 'cut --help')]) 21 | def test_get_new_command(before, after): 22 | assert get_new_command(Command(before, '')) == after 23 | -------------------------------------------------------------------------------- /thefuck/rules/git_rm_staged.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return (' rm ' in command.script and 7 | 'error: the following file has changes staged in the index' in command.output and 8 | 'use --cached to keep the file, or -f to force removal' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | command_parts = command.script_parts[:] 14 | index = command_parts.index('rm') + 1 15 | command_parts.insert(index, '--cached') 16 | command_list = [u' '.join(command_parts)] 17 | command_parts[index] = '-f' 18 | command_list.append(u' '.join(command_parts)) 19 | return command_list 20 | -------------------------------------------------------------------------------- /thefuck/rules/git_rm_local_modifications.py: -------------------------------------------------------------------------------- 1 | from thefuck.specific.git import git_support 2 | 3 | 4 | @git_support 5 | def match(command): 6 | return (' rm ' in command.script and 7 | 'error: the following file has local modifications' in command.output and 8 | 'use --cached to keep the file, or -f to force removal' in command.output) 9 | 10 | 11 | @git_support 12 | def get_new_command(command): 13 | command_parts = command.script_parts[:] 14 | index = command_parts.index('rm') + 1 15 | command_parts.insert(index, '--cached') 16 | command_list = [u' '.join(command_parts)] 17 | command_parts[index] = '-f' 18 | command_list.append(u' '.join(command_parts)) 19 | return command_list 20 | -------------------------------------------------------------------------------- /tests/rules/test_has_exists_script.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | from thefuck.rules.has_exists_script import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | with patch('os.path.exists', return_value=True): 8 | assert match(Command('main', 'main: command not found')) 9 | assert match(Command('main --help', 10 | 'main: command not found')) 11 | assert not match(Command('main', '')) 12 | 13 | with patch('os.path.exists', return_value=False): 14 | assert not match(Command('main', 'main: command not found')) 15 | 16 | 17 | def test_get_new_command(): 18 | assert get_new_command(Command('main --help', '')) == './main --help' 19 | -------------------------------------------------------------------------------- /thefuck/rules/lein_not_task.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_command, get_all_matched_commands, for_app 3 | from thefuck.specific.sudo import sudo_support 4 | 5 | 6 | @sudo_support 7 | @for_app('lein') 8 | def match(command): 9 | return (command.script.startswith('lein') 10 | and "is not a task. See 'lein help'" in command.output 11 | and 'Did you mean this?' in command.output) 12 | 13 | 14 | @sudo_support 15 | def get_new_command(command): 16 | broken_cmd = re.findall(r"'([^']*)' is not a task", 17 | command.output)[0] 18 | new_cmds = get_all_matched_commands(command.output, 'Did you mean this?') 19 | return replace_command(command, broken_cmd, new_cmds) 20 | -------------------------------------------------------------------------------- /thefuck/rules/gulp_not_task.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | from thefuck.utils import replace_command, for_app, cache 4 | 5 | 6 | @for_app('gulp') 7 | def match(command): 8 | return 'is not in your gulpfile' in command.output 9 | 10 | 11 | @cache('gulpfile.js') 12 | def get_gulp_tasks(): 13 | proc = subprocess.Popen(['gulp', '--tasks-simple'], 14 | stdout=subprocess.PIPE) 15 | return [line.decode('utf-8')[:-1] 16 | for line in proc.stdout.readlines()] 17 | 18 | 19 | def get_new_command(command): 20 | wrong_task = re.findall(r"Task '(\w+)' is not in your gulpfile", 21 | command.output)[0] 22 | return replace_command(command, wrong_task, get_gulp_tasks()) 23 | -------------------------------------------------------------------------------- /tests/specific/test_sudo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('return_value, command, called, result', [ 7 | ('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'), 8 | ('ls -lah', 'ls', 'ls', 'ls -lah'), 9 | (['ls -lah'], 'sudo ls', 'ls', ['sudo ls -lah']), 10 | (True, 'sudo ls', 'ls', True), 11 | (True, 'ls', 'ls', True), 12 | (False, 'sudo ls', 'ls', False), 13 | (False, 'ls', 'ls', False)]) 14 | def test_sudo_support(return_value, command, called, result): 15 | def fn(command): 16 | assert command == Command(called, '') 17 | return return_value 18 | 19 | assert sudo_support(fn)(Command(command, '')) == result 20 | -------------------------------------------------------------------------------- /thefuck/rules/docker_image_being_used_by_container.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | from thefuck.shells import shell 3 | 4 | 5 | @for_app('docker') 6 | def match(command): 7 | ''' 8 | Matches a command's output with docker's output 9 | warning you that you need to remove a container before removing an image. 10 | ''' 11 | return 'image is being used by running container' in command.output 12 | 13 | 14 | def get_new_command(command): 15 | ''' 16 | Prepends docker container rm -f {container ID} to 17 | the previous docker image rm {image ID} command 18 | ''' 19 | container_id = command.output.strip().split(' ') 20 | return shell.and_('docker container rm -f {}', '{}').format(container_id[-1], command.script) 21 | -------------------------------------------------------------------------------- /thefuck/rules/git_hook_bypass.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | from thefuck.specific.git import git_support 3 | 4 | hooked_commands = ("am", "commit", "push") 5 | 6 | 7 | @git_support 8 | def match(command): 9 | return any( 10 | hooked_command in command.script_parts for hooked_command in hooked_commands 11 | ) 12 | 13 | 14 | @git_support 15 | def get_new_command(command): 16 | hooked_command = next( 17 | hooked_command 18 | for hooked_command in hooked_commands 19 | if hooked_command in command.script_parts 20 | ) 21 | return replace_argument( 22 | command.script, hooked_command, hooked_command + " --no-verify" 23 | ) 24 | 25 | 26 | priority = 1100 27 | requires_output = False 28 | -------------------------------------------------------------------------------- /thefuck/rules/git_not_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import get_all_matched_commands, replace_command 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return (" is not a git command. See 'git --help'." in command.output 9 | and ('The most similar command' in command.output 10 | or 'Did you mean' in command.output)) 11 | 12 | 13 | @git_support 14 | def get_new_command(command): 15 | broken_cmd = re.findall(r"git: '([^']*)' is not a git command", 16 | command.output)[0] 17 | matched = get_all_matched_commands(command.output, ['The most similar command', 'Did you mean']) 18 | return replace_command(command, broken_cmd, matched) 19 | -------------------------------------------------------------------------------- /tests/rules/test_git_add_force.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_add_force import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return ('The following paths are ignored by one of your .gitignore files:\n' 9 | 'dist/app.js\n' 10 | 'dist/background.js\n' 11 | 'dist/options.js\n' 12 | 'Use -f if you really want to add them.\n') 13 | 14 | 15 | def test_match(output): 16 | assert match(Command('git add dist/*.js', output)) 17 | assert not match(Command('git add dist/*.js', '')) 18 | 19 | 20 | def test_get_new_command(output): 21 | assert (get_new_command(Command('git add dist/*.js', output)) 22 | == "git add --force dist/*.js") 23 | -------------------------------------------------------------------------------- /tests/rules/test_git_clone_git_clone.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.git_clone_git_clone import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | output_clean = """ 6 | fatal: Too many arguments. 7 | 8 | usage: git clone [] [--] [] 9 | """ 10 | 11 | 12 | def test_match(): 13 | assert match(Command('git clone git clone foo', output_clean)) 14 | 15 | 16 | def test_not_match(): 17 | assert not match(Command('', '')) 18 | assert not match(Command('git branch', '')) 19 | assert not match(Command('git clone foo', '')) 20 | assert not match(Command('git clone foo bar baz', output_clean)) 21 | 22 | 23 | def test_get_new_command(): 24 | assert get_new_command(Command('git clone git clone foo', output_clean)) == 'git clone foo' 25 | -------------------------------------------------------------------------------- /thefuck/rules/prove_recursively.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.utils import for_app 3 | 4 | 5 | def _is_recursive(part): 6 | if part == '--recurse': 7 | return True 8 | elif not part.startswith('--') and part.startswith('-') and 'r' in part: 9 | return True 10 | 11 | 12 | def _isdir(part): 13 | return not part.startswith('-') and os.path.isdir(part) 14 | 15 | 16 | @for_app('prove') 17 | def match(command): 18 | return ( 19 | 'NOTESTS' in command.output 20 | and not any(_is_recursive(part) for part in command.script_parts[1:]) 21 | and any(_isdir(part) for part in command.script_parts[1:])) 22 | 23 | 24 | def get_new_command(command): 25 | parts = command.script_parts[:] 26 | parts.insert(1, '-r') 27 | return u' '.join(parts) 28 | -------------------------------------------------------------------------------- /tests/rules/test_cp_omitting_directory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.cp_omitting_directory import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('script, output', [ 7 | ('cp dir', 'cp: dor: is a directory'), 8 | ('cp dir', "cp: omitting directory 'dir'")]) 9 | def test_match(script, output): 10 | assert match(Command(script, output)) 11 | 12 | 13 | @pytest.mark.parametrize('script, output', [ 14 | ('some dir', 'cp: dor: is a directory'), 15 | ('some dir', "cp: omitting directory 'dir'"), 16 | ('cp dir', '')]) 17 | def test_not_match(script, output): 18 | assert not match(Command(script, output)) 19 | 20 | 21 | def test_get_new_command(): 22 | assert get_new_command(Command('cp dir', '')) == 'cp -a dir' 23 | -------------------------------------------------------------------------------- /tests/shells/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def builtins_open(mocker): 6 | return mocker.patch('six.moves.builtins.open') 7 | 8 | 9 | @pytest.fixture 10 | def isfile(mocker): 11 | return mocker.patch('os.path.isfile', return_value=True) 12 | 13 | 14 | @pytest.fixture 15 | @pytest.mark.usefixtures('isfile') 16 | def history_lines(mocker): 17 | def aux(lines): 18 | mock = mocker.patch('io.open') 19 | mock.return_value.__enter__ \ 20 | .return_value.readlines.return_value = lines 21 | 22 | return aux 23 | 24 | 25 | @pytest.fixture 26 | def config_exists(mocker): 27 | path_mock = mocker.patch('thefuck.shells.generic.Path') 28 | return path_mock.return_value \ 29 | .expanduser.return_value \ 30 | .exists 31 | -------------------------------------------------------------------------------- /tests/rules/test_git_pull_clone.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_pull_clone import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | git_err = ''' 7 | fatal: Not a git repository (or any parent up to mount point /home) 8 | Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). 9 | ''' 10 | 11 | 12 | @pytest.mark.parametrize('command', [ 13 | Command('git pull git@github.com:mcarton/thefuck.git', git_err)]) 14 | def test_match(command): 15 | assert match(command) 16 | 17 | 18 | @pytest.mark.parametrize('command, output', [ 19 | (Command('git pull git@github.com:mcarton/thefuck.git', git_err), 'git clone git@github.com:mcarton/thefuck.git')]) 20 | def test_get_new_command(command, output): 21 | assert get_new_command(command) == output 22 | -------------------------------------------------------------------------------- /tests/rules/test_unsudo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.unsudo import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('output', [ 7 | 'you cannot perform this operation as root']) 8 | def test_match(output): 9 | assert match(Command('sudo ls', output)) 10 | 11 | 12 | def test_not_match(): 13 | assert not match(Command('', '')) 14 | assert not match(Command('sudo ls', 'Permission denied')) 15 | assert not match(Command('ls', 'you cannot perform this operation as root')) 16 | 17 | 18 | @pytest.mark.parametrize('before, after', [ 19 | ('sudo ls', 'ls'), 20 | ('sudo pacaur -S helloworld', 'pacaur -S helloworld')]) 21 | def test_get_new_command(before, after): 22 | assert get_new_command(Command(before, '')) == after 23 | -------------------------------------------------------------------------------- /thefuck/rules/git_push_pull.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.utils import replace_argument 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | return ('push' in command.script and 9 | '! [rejected]' in command.output and 10 | 'failed to push some refs to' in command.output and 11 | ('Updates were rejected because the tip of your' 12 | ' current branch is behind' in command.output or 13 | 'Updates were rejected because the remote ' 14 | 'contains work that you do' in command.output)) 15 | 16 | 17 | @git_support 18 | def get_new_command(command): 19 | return shell.and_(replace_argument(command.script, 'push', 'pull'), 20 | command.script) 21 | -------------------------------------------------------------------------------- /tests/rules/test_git_diff_no_index.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_diff_no_index import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('git diff foo bar', '')]) 8 | def test_match(command): 9 | assert match(command) 10 | 11 | 12 | @pytest.mark.parametrize('command', [ 13 | Command('git diff --no-index foo bar', ''), 14 | Command('git diff foo', ''), 15 | Command('git diff foo bar baz', '')]) 16 | def test_not_match(command): 17 | assert not match(command) 18 | 19 | 20 | @pytest.mark.parametrize('command, new_command', [ 21 | (Command('git diff foo bar', ''), 'git diff --no-index foo bar')]) 22 | def test_get_new_command(command, new_command): 23 | assert get_new_command(command) == new_command 24 | -------------------------------------------------------------------------------- /thefuck/rules/long_form_help.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import replace_argument 2 | import re 3 | 4 | # regex to match a suggested help command from the tool output 5 | help_regex = r"(?:Run|Try) '([^']+)'(?: or '[^']+')? for (?:details|more information)." 6 | 7 | 8 | def match(command): 9 | if re.search(help_regex, command.output, re.I) is not None: 10 | return True 11 | 12 | if '--help' in command.output: 13 | return True 14 | 15 | return False 16 | 17 | 18 | def get_new_command(command): 19 | if re.search(help_regex, command.output) is not None: 20 | match_obj = re.search(help_regex, command.output, re.I) 21 | return match_obj.group(1) 22 | 23 | return replace_argument(command.script, '-h', '--help') 24 | 25 | 26 | enabled_by_default = True 27 | priority = 5000 28 | -------------------------------------------------------------------------------- /tests/rules/test_git_commit_amend.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_commit_amend import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('script, output', [ 7 | ('git commit -m "test"', 'test output'), 8 | ('git commit', '')]) 9 | def test_match(output, script): 10 | assert match(Command(script, output)) 11 | 12 | 13 | @pytest.mark.parametrize('script', [ 14 | 'git branch foo', 15 | 'git checkout feature/test_commit', 16 | 'git push']) 17 | def test_not_match(script): 18 | assert not match(Command(script, '')) 19 | 20 | 21 | @pytest.mark.parametrize('script', [ 22 | ('git commit -m "test commit"'), 23 | ('git commit')]) 24 | def test_get_new_command(script): 25 | assert get_new_command(Command(script, '')) == 'git commit --amend' 26 | -------------------------------------------------------------------------------- /tests/rules/test_git_commit_reset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_commit_reset import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('script, output', [ 7 | ('git commit -m "test"', 'test output'), 8 | ('git commit', '')]) 9 | def test_match(output, script): 10 | assert match(Command(script, output)) 11 | 12 | 13 | @pytest.mark.parametrize('script', [ 14 | 'git branch foo', 15 | 'git checkout feature/test_commit', 16 | 'git push']) 17 | def test_not_match(script): 18 | assert not match(Command(script, '')) 19 | 20 | 21 | @pytest.mark.parametrize('script', [ 22 | ('git commit -m "test commit"'), 23 | ('git commit')]) 24 | def test_get_new_command(script): 25 | assert get_new_command(Command(script, '')) == 'git reset HEAD~' 26 | -------------------------------------------------------------------------------- /tests/rules/test_git_remote_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_remote_delete import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | assert match(Command('git remote delete foo', '')) 8 | 9 | 10 | @pytest.mark.parametrize('command', [ 11 | Command('git remote remove foo', ''), 12 | Command('git remote add foo', ''), 13 | Command('git commit', '') 14 | ]) 15 | def test_not_match(command): 16 | assert not match(command) 17 | 18 | 19 | @pytest.mark.parametrize('command, new_command', [ 20 | (Command('git remote delete foo', ''), 'git remote remove foo'), 21 | (Command('git remote delete delete', ''), 'git remote remove delete'), 22 | ]) 23 | def test_get_new_command(command, new_command): 24 | assert get_new_command(command) == new_command 25 | -------------------------------------------------------------------------------- /thefuck/rules/ifconfig_device_not_found.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from thefuck.utils import for_app, replace_command, eager 3 | 4 | 5 | @for_app('ifconfig') 6 | def match(command): 7 | return 'error fetching interface information: Device not found' \ 8 | in command.output 9 | 10 | 11 | @eager 12 | def _get_possible_interfaces(): 13 | proc = subprocess.Popen(['ifconfig', '-a'], stdout=subprocess.PIPE) 14 | for line in proc.stdout.readlines(): 15 | line = line.decode() 16 | if line and line != '\n' and not line.startswith(' '): 17 | yield line.split(' ')[0] 18 | 19 | 20 | def get_new_command(command): 21 | interface = command.output.split(' ')[0][:-1] 22 | possible_interfaces = _get_possible_interfaces() 23 | return replace_command(command, interface, possible_interfaces) 24 | -------------------------------------------------------------------------------- /tests/rules/test_git_push_without_commits.py: -------------------------------------------------------------------------------- 1 | from thefuck.types import Command 2 | from thefuck.rules.git_push_without_commits import get_new_command, match 3 | 4 | 5 | def test_match(): 6 | script = "git push -u origin master" 7 | output = "error: src refspec master does not match any\nerror: failed to..." 8 | assert match(Command(script, output)) 9 | 10 | 11 | def test_not_match(): 12 | script = "git push -u origin master" 13 | assert not match(Command(script, "Everything up-to-date")) 14 | 15 | 16 | def test_get_new_command(): 17 | script = "git push -u origin master" 18 | output = "error: src refspec master does not match any\nerror: failed to..." 19 | new_command = 'git commit -m "Initial commit" && git push -u origin master' 20 | assert get_new_command(Command(script, output)) == new_command 21 | -------------------------------------------------------------------------------- /thefuck/rules/scm_correction.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app, memoize 2 | from thefuck.system import Path 3 | 4 | path_to_scm = { 5 | '.git': 'git', 6 | '.hg': 'hg', 7 | } 8 | 9 | wrong_scm_patterns = { 10 | 'git': 'fatal: Not a git repository', 11 | 'hg': 'abort: no repository found', 12 | } 13 | 14 | 15 | @memoize 16 | def _get_actual_scm(): 17 | for path, scm in path_to_scm.items(): 18 | if Path(path).is_dir(): 19 | return scm 20 | 21 | 22 | @for_app(*wrong_scm_patterns.keys()) 23 | def match(command): 24 | scm = command.script_parts[0] 25 | pattern = wrong_scm_patterns[scm] 26 | 27 | return pattern in command.output and _get_actual_scm() 28 | 29 | 30 | def get_new_command(command): 31 | scm = _get_actual_scm() 32 | return u' '.join([scm] + command.script_parts[1:]) 33 | -------------------------------------------------------------------------------- /thefuck/rules/pacman_not_found.py: -------------------------------------------------------------------------------- 1 | """ Fixes wrong package names with pacman or yaourt. 2 | 3 | For example the `llc` program is in package `llvm` so this: 4 | yay -S llc 5 | should be: 6 | yay -S llvm 7 | """ 8 | 9 | from thefuck.utils import replace_command 10 | from thefuck.specific.archlinux import get_pkgfile, archlinux_env 11 | 12 | 13 | def match(command): 14 | return (command.script_parts 15 | and (command.script_parts[0] in ('pacman', 'yay', 'pikaur', 'yaourt') 16 | or command.script_parts[0:2] == ['sudo', 'pacman']) 17 | and 'error: target not found:' in command.output) 18 | 19 | 20 | def get_new_command(command): 21 | pgr = command.script_parts[-1] 22 | 23 | return replace_command(command, pgr, get_pkgfile(pgr)) 24 | 25 | 26 | enabled_by_default, _ = archlinux_env() 27 | -------------------------------------------------------------------------------- /tests/rules/test_fix_alt_space.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from thefuck.rules.fix_alt_space import match, get_new_command 4 | from thefuck.types import Command 5 | 6 | 7 | def test_match(): 8 | """The character before 'grep' is Alt+Space, which happens frequently 9 | on the Mac when typing the pipe character (Alt+7), and holding the Alt 10 | key pressed for longer than necessary. 11 | 12 | """ 13 | assert match(Command(u'ps -ef | grep foo', 14 | u'-bash:  grep: command not found')) 15 | assert not match(Command('ps -ef | grep foo', '')) 16 | assert not match(Command('', '')) 17 | 18 | 19 | def test_get_new_command(): 20 | """ Replace the Alt+Space character by a simple space """ 21 | assert (get_new_command(Command(u'ps -ef | grep foo', '')) 22 | == 'ps -ef | grep foo') 23 | -------------------------------------------------------------------------------- /tests/rules/test_ag_literal.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.ag_literal import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return ('ERR: Bad regex! pcre_compile() failed at position 1: missing )\n' 9 | 'If you meant to search for a literal string, run ag with -Q\n') 10 | 11 | 12 | @pytest.mark.parametrize('script', ['ag \\(']) 13 | def test_match(script, output): 14 | assert match(Command(script, output)) 15 | 16 | 17 | @pytest.mark.parametrize('script', ['ag foo']) 18 | def test_not_match(script): 19 | assert not match(Command(script, '')) 20 | 21 | 22 | @pytest.mark.parametrize('script, new_cmd', [ 23 | ('ag \\(', 'ag -Q \\(')]) 24 | def test_get_new_command(script, new_cmd, output): 25 | assert get_new_command((Command(script, output))) == new_cmd 26 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from thefuck import types 2 | from thefuck.const import DEFAULT_PRIORITY 3 | 4 | 5 | class Rule(types.Rule): 6 | def __init__(self, name='', match=lambda *_: True, 7 | get_new_command=lambda *_: '', 8 | enabled_by_default=True, 9 | side_effect=None, 10 | priority=DEFAULT_PRIORITY, 11 | requires_output=True): 12 | super(Rule, self).__init__(name, match, get_new_command, 13 | enabled_by_default, side_effect, 14 | priority, requires_output) 15 | 16 | 17 | class CorrectedCommand(types.CorrectedCommand): 18 | def __init__(self, script='', side_effect=None, priority=DEFAULT_PRIORITY): 19 | super(CorrectedCommand, self).__init__( 20 | script, side_effect, priority) 21 | -------------------------------------------------------------------------------- /tests/rules/test_apt_get_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.apt_get_search import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | 6 | def test_match(): 7 | assert match(Command('apt-get search foo', '')) 8 | 9 | 10 | @pytest.mark.parametrize('command', [ 11 | Command('apt-cache search foo', ''), 12 | Command('aptitude search foo', ''), 13 | Command('apt search foo', ''), 14 | Command('apt-get install foo', ''), 15 | Command('apt-get source foo', ''), 16 | Command('apt-get clean', ''), 17 | Command('apt-get remove', ''), 18 | Command('apt-get update', '') 19 | ]) 20 | def test_not_match(command): 21 | assert not match(command) 22 | 23 | 24 | def test_get_new_command(): 25 | new_command = get_new_command(Command('apt-get search foo', '')) 26 | assert new_command == 'apt-cache search foo' 27 | -------------------------------------------------------------------------------- /tests/rules/test_quotation_marks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.quotation_marks import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command("git commit -m \'My Message\"", ''), 8 | Command("git commit -am \"Mismatched Quotation Marks\'", ''), 9 | Command("echo \"hello\'", '')]) 10 | def test_match(command): 11 | assert match(command) 12 | 13 | 14 | @pytest.mark.parametrize('command, new_command', [ 15 | (Command("git commit -m \'My Message\"", ''), "git commit -m \"My Message\""), 16 | (Command("git commit -am \"Mismatched Quotation Marks\'", ''), "git commit -am \"Mismatched Quotation Marks\""), 17 | (Command("echo \"hello\'", ''), "echo \"hello\"")]) 18 | def test_get_new_command(command, new_command): 19 | assert get_new_command(command) == new_command 20 | -------------------------------------------------------------------------------- /tests/specific/test_npm.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | import pytest 4 | from thefuck.specific.npm import get_scripts 5 | 6 | run_script_stdout = b''' 7 | Lifecycle scripts included in code-view-web: 8 | test 9 | jest 10 | 11 | available via `npm run-script`: 12 | build 13 | cp node_modules/ace-builds/src-min/ -a resources/ace/ && webpack --progress --colors -p --config ./webpack.production.config.js 14 | develop 15 | cp node_modules/ace-builds/src/ -a resources/ace/ && webpack-dev-server --progress --colors 16 | watch-test 17 | jest --verbose --watch 18 | 19 | ''' 20 | 21 | 22 | @pytest.mark.usefixtures('no_memoize') 23 | def test_get_scripts(mocker): 24 | patch = mocker.patch('thefuck.specific.npm.Popen') 25 | patch.return_value.stdout = BytesIO(run_script_stdout) 26 | assert get_scripts() == ['build', 'develop', 'watch-test'] 27 | -------------------------------------------------------------------------------- /thefuck/rules/git_add.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | from thefuck.specific.git import git_support 4 | from thefuck.system import Path 5 | from thefuck.utils import memoize 6 | 7 | 8 | @memoize 9 | def _get_missing_file(command): 10 | pathspec = re.findall( 11 | r"error: pathspec '([^']*)' " 12 | r'did not match any file\(s\) known to git.', command.output)[0] 13 | if Path(pathspec).exists(): 14 | return pathspec 15 | 16 | 17 | @git_support 18 | def match(command): 19 | return ('did not match any file(s) known to git.' in command.output 20 | and _get_missing_file(command)) 21 | 22 | 23 | @git_support 24 | def get_new_command(command): 25 | missing_file = _get_missing_file(command) 26 | formatme = shell.and_('git add -- {}', '{}') 27 | return formatme.format(missing_file, command.script) 28 | -------------------------------------------------------------------------------- /tests/rules/test_cd_correction.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.cd_correction import match 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('cd foo', 'cd: foo: No such file or directory'), 8 | Command('cd foo/bar/baz', 9 | 'cd: foo: No such file or directory'), 10 | Command('cd foo/bar/baz', 'cd: can\'t cd to foo/bar/baz'), 11 | Command('cd /foo/bar/', 'cd: The directory "/foo/bar/" does not exist')]) 12 | def test_match(command): 13 | assert match(command) 14 | 15 | 16 | @pytest.mark.parametrize('command', [ 17 | Command('cd foo', ''), Command('', '')]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | # Note that get_new_command uses local filesystem, so not testing it here. 23 | # Instead, see the functional test `functional.test_cd_correction` 24 | -------------------------------------------------------------------------------- /tests/rules/test_git_lfs_mistype.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thefuck.rules.git_lfs_mistype import match, get_new_command 4 | from thefuck.types import Command 5 | 6 | 7 | @pytest.fixture 8 | def mistype_response(): 9 | return """ 10 | Error: unknown command "evn" for "git-lfs" 11 | 12 | Did you mean this? 13 | env 14 | ext 15 | 16 | Run 'git-lfs --help' for usage. 17 | """ 18 | 19 | 20 | def test_match(mistype_response): 21 | assert match(Command('git lfs evn', mistype_response)) 22 | err_response = 'bash: git: command not found' 23 | assert not match(Command('git lfs env', err_response)) 24 | assert not match(Command('docker lfs env', mistype_response)) 25 | 26 | 27 | def test_get_new_command(mistype_response): 28 | assert (get_new_command(Command('git lfs evn', mistype_response)) 29 | == ['git lfs env', 'git lfs ext']) 30 | -------------------------------------------------------------------------------- /thefuck/entrypoints/alias.py: -------------------------------------------------------------------------------- 1 | import six 2 | from ..conf import settings 3 | from ..logs import warn 4 | from ..shells import shell 5 | from ..utils import which 6 | 7 | 8 | def _get_alias(known_args): 9 | if six.PY2: 10 | warn("The Fuck will drop Python 2 support soon, more details " 11 | "https://github.com/nvbn/thefuck/issues/685") 12 | 13 | alias = shell.app_alias(known_args.alias) 14 | 15 | if known_args.enable_experimental_instant_mode: 16 | if six.PY2: 17 | warn("Instant mode requires Python 3") 18 | elif not which('script'): 19 | warn("Instant mode requires `script` app") 20 | else: 21 | return shell.instant_mode_alias(known_args.alias) 22 | 23 | return alias 24 | 25 | 26 | def print_alias(known_args): 27 | settings.init(known_args) 28 | print(_get_alias(known_args)) 29 | -------------------------------------------------------------------------------- /thefuck/rules/hostscli.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.sudo import sudo_support 3 | from thefuck.utils import replace_command, for_app 4 | 5 | no_command = "Error: No such command" 6 | no_website = "hostscli.errors.WebsiteImportError" 7 | 8 | 9 | @sudo_support 10 | @for_app('hostscli') 11 | def match(command): 12 | errors = [no_command, no_website] 13 | for error in errors: 14 | if error in command.output: 15 | return True 16 | return False 17 | 18 | 19 | @sudo_support 20 | def get_new_command(command): 21 | if no_website in command.output: 22 | return ['hostscli websites'] 23 | 24 | misspelled_command = re.findall( 25 | r'Error: No such command ".*"', command.output)[0] 26 | commands = ['block', 'unblock', 'websites', 'block_all', 'unblock_all'] 27 | return replace_command(command, misspelled_command, commands) 28 | -------------------------------------------------------------------------------- /tests/rules/test_git_diff_staged.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_diff_staged import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('git diff foo', ''), 8 | Command('git diff', '')]) 9 | def test_match(command): 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command', [ 14 | Command('git diff --staged', ''), 15 | Command('git tag', ''), 16 | Command('git branch', ''), 17 | Command('git log', '')]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | @pytest.mark.parametrize('command, new_command', [ 23 | (Command('git diff', ''), 'git diff --staged'), 24 | (Command('git diff foo', ''), 'git diff --staged foo')]) 25 | def test_get_new_command(command, new_command): 26 | assert get_new_command(command) == new_command 27 | -------------------------------------------------------------------------------- /tests/rules/test_php_s.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.php_s import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('php -s localhost:8000', ''), 8 | Command('php -t pub -s 0.0.0.0:8080', '') 9 | ]) 10 | def test_match(command): 11 | assert match(command) 12 | 13 | 14 | @pytest.mark.parametrize('command', [ 15 | Command('php -S localhost:8000', ''), 16 | Command('vim php -s', '') 17 | ]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | @pytest.mark.parametrize('command, new_command', [ 23 | (Command('php -s localhost:8000', ''), 'php -S localhost:8000'), 24 | (Command('php -t pub -s 0.0.0.0:8080', ''), 'php -t pub -S 0.0.0.0:8080') 25 | ]) 26 | def test_get_new_command(command, new_command): 27 | assert get_new_command(command) == new_command 28 | -------------------------------------------------------------------------------- /tests/rules/test_history.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.history import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def history_without_current(mocker): 8 | return mocker.patch( 9 | 'thefuck.rules.history.get_valid_history_without_current', 10 | return_value=['ls cat', 'diff x']) 11 | 12 | 13 | @pytest.mark.parametrize('script', ['ls cet', 'daff x']) 14 | def test_match(script): 15 | assert match(Command(script, '')) 16 | 17 | 18 | @pytest.mark.parametrize('script', ['apt-get', 'nocommand y']) 19 | def test_not_match(script): 20 | assert not match(Command(script, '')) 21 | 22 | 23 | @pytest.mark.parametrize('script, result', [ 24 | ('ls cet', 'ls cat'), 25 | ('daff x', 'diff x')]) 26 | def test_get_new_command(script, result): 27 | assert get_new_command(Command(script, '')) == result 28 | -------------------------------------------------------------------------------- /thefuck/rules/brew_install.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | from thefuck.specific.brew import brew_available 4 | 5 | enabled_by_default = brew_available 6 | 7 | 8 | def _get_suggestions(str): 9 | suggestions = str.replace(" or ", ", ").split(", ") 10 | return suggestions 11 | 12 | 13 | @for_app('brew', at_least=2) 14 | def match(command): 15 | is_proper_command = ('install' in command.script and 16 | 'No available formula' in command.output and 17 | 'Did you mean' in command.output) 18 | return is_proper_command 19 | 20 | 21 | def get_new_command(command): 22 | matcher = re.search('Warning: No available formula with the name "(?:[^"]+)". Did you mean (.+)\\?', command.output) 23 | suggestions = _get_suggestions(matcher.group(1)) 24 | return ["brew install " + formula for formula in suggestions] 25 | -------------------------------------------------------------------------------- /tests/rules/test_cargo_no_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.cargo_no_command import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | no_such_subcommand_old = """No such subcommand 7 | 8 | Did you mean `build`? 9 | """ 10 | 11 | no_such_subcommand = """error: no such subcommand 12 | 13 | \tDid you mean `build`? 14 | """ 15 | 16 | 17 | @pytest.mark.parametrize('command', [ 18 | Command('cargo buid', no_such_subcommand_old), 19 | Command('cargo buils', no_such_subcommand)]) 20 | def test_match(command): 21 | assert match(command) 22 | 23 | 24 | @pytest.mark.parametrize('command, new_command', [ 25 | (Command('cargo buid', no_such_subcommand_old), 'cargo build'), 26 | (Command('cargo buils', no_such_subcommand), 'cargo build')]) 27 | def test_get_new_command(command, new_command): 28 | assert get_new_command(command) == new_command 29 | -------------------------------------------------------------------------------- /thefuck/rules/ln_s_order.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thefuck.specific.sudo import sudo_support 3 | 4 | 5 | def _get_destination(script_parts): 6 | """When arguments order is wrong first argument will be destination.""" 7 | for part in script_parts: 8 | if part not in {'ln', '-s', '--symbolic'} and os.path.exists(part): 9 | return part 10 | 11 | 12 | @sudo_support 13 | def match(command): 14 | return (command.script_parts[0] == 'ln' 15 | and {'-s', '--symbolic'}.intersection(command.script_parts) 16 | and 'File exists' in command.output 17 | and _get_destination(command.script_parts)) 18 | 19 | 20 | @sudo_support 21 | def get_new_command(command): 22 | destination = _get_destination(command.script_parts) 23 | parts = command.script_parts[:] 24 | parts.remove(destination) 25 | parts.append(destination) 26 | return ' '.join(parts) 27 | -------------------------------------------------------------------------------- /tests/rules/test_yarn_alias.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.yarn_alias import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | output_remove = 'error Did you mean `yarn remove`?' 7 | output_etl = 'error Command "etil" not found. Did you mean "etl"?' 8 | output_list = 'error Did you mean `yarn list`?' 9 | 10 | 11 | @pytest.mark.parametrize('command', [ 12 | Command('yarn rm', output_remove), 13 | Command('yarn etil', output_etl), 14 | Command('yarn ls', output_list)]) 15 | def test_match(command): 16 | assert match(command) 17 | 18 | 19 | @pytest.mark.parametrize('command, new_command', [ 20 | (Command('yarn rm', output_remove), 'yarn remove'), 21 | (Command('yarn etil', output_etl), 'yarn etl'), 22 | (Command('yarn ls', output_list), 'yarn list')]) 23 | def test_get_new_command(command, new_command): 24 | assert get_new_command(command) == new_command 25 | -------------------------------------------------------------------------------- /tests/rules/test_git_merge.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_merge import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | output = 'merge: local - not something we can merge\n\n' \ 7 | 'Did you mean this?\n\tremote/local' 8 | 9 | 10 | def test_match(): 11 | assert match(Command('git merge test', output)) 12 | assert not match(Command('git merge master', '')) 13 | assert not match(Command('ls', output)) 14 | 15 | 16 | @pytest.mark.parametrize('command, new_command', [ 17 | (Command('git merge local', output), 18 | 'git merge remote/local'), 19 | (Command('git merge -m "test" local', output), 20 | 'git merge -m "test" remote/local'), 21 | (Command('git merge -m "test local" local', output), 22 | 'git merge -m "test local" remote/local')]) 23 | def test_get_new_command(command, new_command): 24 | assert get_new_command(command) == new_command 25 | -------------------------------------------------------------------------------- /thefuck/rules/git_branch_0flag.py: -------------------------------------------------------------------------------- 1 | from thefuck.shells import shell 2 | from thefuck.specific.git import git_support 3 | from thefuck.utils import memoize 4 | 5 | 6 | @memoize 7 | def first_0flag(script_parts): 8 | return next((p for p in script_parts if len(p) == 2 and p.startswith("0")), None) 9 | 10 | 11 | @git_support 12 | def match(command): 13 | return command.script_parts[1] == "branch" and first_0flag(command.script_parts) 14 | 15 | 16 | @git_support 17 | def get_new_command(command): 18 | branch_name = first_0flag(command.script_parts) 19 | fixed_flag = branch_name.replace("0", "-") 20 | fixed_script = command.script.replace(branch_name, fixed_flag) 21 | if "A branch named '" in command.output and "' already exists." in command.output: 22 | delete_branch = u"git branch -D {}".format(branch_name) 23 | return shell.and_(delete_branch, fixed_script) 24 | return fixed_script 25 | -------------------------------------------------------------------------------- /thefuck/rules/no_such_file.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | 4 | 5 | patterns = ( 6 | r"mv: cannot move '[^']*' to '([^']*)': No such file or directory", 7 | r"mv: cannot move '[^']*' to '([^']*)': Not a directory", 8 | r"cp: cannot create regular file '([^']*)': No such file or directory", 9 | r"cp: cannot create regular file '([^']*)': Not a directory", 10 | ) 11 | 12 | 13 | def match(command): 14 | for pattern in patterns: 15 | if re.search(pattern, command.output): 16 | return True 17 | 18 | return False 19 | 20 | 21 | def get_new_command(command): 22 | for pattern in patterns: 23 | file = re.findall(pattern, command.output) 24 | 25 | if file: 26 | file = file[0] 27 | dir = file[0:file.rfind('/')] 28 | 29 | formatme = shell.and_('mkdir -p {}', '{}') 30 | return formatme.format(dir, command.script) 31 | -------------------------------------------------------------------------------- /tests/rules/test_git_pull.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_pull import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''There is no tracking information for the current branch. 9 | Please specify which branch you want to merge with. 10 | See git-pull(1) for details 11 | 12 | git pull 13 | 14 | If you wish to set tracking information for this branch you can do so with: 15 | 16 | git branch --set-upstream-to=/ master 17 | 18 | ''' 19 | 20 | 21 | def test_match(output): 22 | assert match(Command('git pull', output)) 23 | assert not match(Command('git pull', '')) 24 | assert not match(Command('ls', output)) 25 | 26 | 27 | def test_get_new_command(output): 28 | assert (get_new_command(Command('git pull', output)) 29 | == "git branch --set-upstream-to=origin/master master && git pull") 30 | -------------------------------------------------------------------------------- /thefuck/rules/mercurial.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import get_closest, for_app 3 | 4 | 5 | def extract_possibilities(command): 6 | possib = re.findall(r'\n\(did you mean one of ([^\?]+)\?\)', command.output) 7 | if possib: 8 | return possib[0].split(', ') 9 | possib = re.findall(r'\n ([^$]+)$', command.output) 10 | if possib: 11 | return possib[0].split(' ') 12 | return possib 13 | 14 | 15 | @for_app('hg') 16 | def match(command): 17 | return ('hg: unknown command' in command.output 18 | and '(did you mean one of ' in command.output 19 | or "hg: command '" in command.output 20 | and "' is ambiguous:" in command.output) 21 | 22 | 23 | def get_new_command(command): 24 | script = command.script_parts[:] 25 | possibilities = extract_possibilities(command) 26 | script[1] = get_closest(script[1], possibilities) 27 | return ' '.join(script) 28 | -------------------------------------------------------------------------------- /tests/rules/test_brew_update_formula.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.types import Command 3 | from thefuck.rules.brew_update_formula import get_new_command, match 4 | 5 | 6 | output = ("Error: This command updates brew itself, and does not take formula" 7 | " names.\nUse `brew upgrade thefuck`.") 8 | 9 | 10 | def test_match(): 11 | command = Command('brew update thefuck', output) 12 | assert match(command) 13 | 14 | 15 | @pytest.mark.parametrize('script', [ 16 | 'brew upgrade foo', 17 | 'brew update']) 18 | def test_not_match(script): 19 | assert not match(Command(script, '')) 20 | 21 | 22 | @pytest.mark.parametrize('script, formula, ', [ 23 | ('brew update foo', 'foo'), 24 | ('brew update bar zap', 'bar zap')]) 25 | def test_get_new_command(script, formula): 26 | command = Command(script, output) 27 | new_command = 'brew upgrade {}'.format(formula) 28 | assert get_new_command(command) == new_command 29 | -------------------------------------------------------------------------------- /tests/rules/test_git_merge_unrelated.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_merge_unrelated import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | output = 'fatal: refusing to merge unrelated histories' 7 | 8 | 9 | def test_match(): 10 | assert match(Command('git merge test', output)) 11 | assert not match(Command('git merge master', '')) 12 | assert not match(Command('ls', output)) 13 | 14 | 15 | @pytest.mark.parametrize('command, new_command', [ 16 | (Command('git merge local', output), 17 | 'git merge local --allow-unrelated-histories'), 18 | (Command('git merge -m "test" local', output), 19 | 'git merge -m "test" local --allow-unrelated-histories'), 20 | (Command('git merge -m "test local" local', output), 21 | 'git merge -m "test local" local --allow-unrelated-histories')]) 22 | def test_get_new_command(command, new_command): 23 | assert get_new_command(command) == new_command 24 | -------------------------------------------------------------------------------- /tests/rules/test_brew_reinstall.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.types import Command 3 | from thefuck.rules.brew_reinstall import get_new_command, match 4 | 5 | 6 | output = ("Warning: thefuck 9.9 is already installed and up-to-date\nTo " 7 | "reinstall 9.9, run `brew reinstall thefuck`") 8 | 9 | 10 | def test_match(): 11 | command = Command('brew install thefuck', output) 12 | assert match(command) 13 | 14 | 15 | @pytest.mark.parametrize('script', [ 16 | 'brew reinstall thefuck', 17 | 'brew install foo']) 18 | def test_not_match(script): 19 | assert not match(Command(script, '')) 20 | 21 | 22 | @pytest.mark.parametrize('script, formula, ', [ 23 | ('brew install foo', 'foo'), 24 | ('brew install bar zap', 'bar zap')]) 25 | def test_get_new_command(script, formula): 26 | command = Command(script, output) 27 | new_command = 'brew reinstall {}'.format(formula) 28 | assert get_new_command(command) == new_command 29 | -------------------------------------------------------------------------------- /thefuck/rules/workon_doesnt_exists.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app, replace_command, eager, memoize 2 | from thefuck.system import Path 3 | 4 | 5 | @memoize 6 | @eager 7 | def _get_all_environments(): 8 | root = Path('~/.virtualenvs').expanduser() 9 | if not root.is_dir(): 10 | return 11 | 12 | for child in root.iterdir(): 13 | if child.is_dir(): 14 | yield child.name 15 | 16 | 17 | @for_app('workon') 18 | def match(command): 19 | return (len(command.script_parts) >= 2 20 | and command.script_parts[1] not in _get_all_environments()) 21 | 22 | 23 | def get_new_command(command): 24 | misspelled_env = command.script_parts[1] 25 | create_new = u'mkvirtualenv {}'.format(misspelled_env) 26 | 27 | available = _get_all_environments() 28 | if available: 29 | return (replace_command(command, misspelled_env, available) 30 | + [create_new]) 31 | else: 32 | return create_new 33 | -------------------------------------------------------------------------------- /tests/rules/test_hostscli.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.hostscli import no_website, get_new_command, match 3 | from thefuck.types import Command 4 | 5 | no_website_long = ''' 6 | {}: 7 | 8 | No Domain list found for website: a_website_that_does_not_exist 9 | 10 | Please raise a Issue here: https://github.com/dhilipsiva/hostscli/issues/new 11 | if you think we should add domains for this website. 12 | 13 | type `hostscli websites` to see a list of websites that you can block/unblock 14 | '''.format(no_website) 15 | 16 | 17 | @pytest.mark.parametrize('command', [ 18 | Command('hostscli block a_website_that_does_not_exist', no_website_long)]) 19 | def test_match(command): 20 | assert match(command) 21 | 22 | 23 | @pytest.mark.parametrize('command, result', [( 24 | Command('hostscli block a_website_that_does_not_exist', no_website_long), 25 | ['hostscli websites'])]) 26 | def test_get_new_command(command, result): 27 | assert get_new_command(command) == result 28 | -------------------------------------------------------------------------------- /tests/rules/test_git_rm_recursive.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_rm_recursive import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(target): 8 | return "fatal: not removing '{}' recursively without -r".format(target) 9 | 10 | 11 | @pytest.mark.parametrize('script, target', [ 12 | ('git rm foo', 'foo'), 13 | ('git rm foo bar', 'foo bar')]) 14 | def test_match(output, script, target): 15 | assert match(Command(script, output)) 16 | 17 | 18 | @pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar']) 19 | def test_not_match(script): 20 | assert not match(Command(script, '')) 21 | 22 | 23 | @pytest.mark.parametrize('script, target, new_command', [ 24 | ('git rm foo', 'foo', 'git rm -r foo'), 25 | ('git rm foo bar', 'foo bar', 'git rm -r foo bar')]) 26 | def test_get_new_command(output, script, target, new_command): 27 | assert get_new_command(Command(script, output)) == new_command 28 | -------------------------------------------------------------------------------- /tests/rules/test_heroku_not_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | from thefuck.types import Command 5 | from thefuck.rules.heroku_not_command import match, get_new_command 6 | 7 | 8 | suggest_output = ''' 9 | ▸ log is not a heroku command. 10 | ▸ Perhaps you meant logs? 11 | ▸ Run heroku _ to run heroku logs. 12 | ▸ Run heroku help for a list of available commands.''' 13 | 14 | 15 | @pytest.mark.parametrize('cmd', ['log']) 16 | def test_match(cmd): 17 | assert match( 18 | Command('heroku {}'.format(cmd), suggest_output)) 19 | 20 | 21 | @pytest.mark.parametrize('script, output', [ 22 | ('cat log', suggest_output)]) 23 | def test_not_match(script, output): 24 | assert not match(Command(script, output)) 25 | 26 | 27 | @pytest.mark.parametrize('cmd, result', [ 28 | ('log', 'heroku logs')]) 29 | def test_get_new_command(cmd, result): 30 | command = Command('heroku {}'.format(cmd), suggest_output) 31 | assert get_new_command(command) == result 32 | -------------------------------------------------------------------------------- /thefuck/rules/man.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app 2 | 3 | 4 | @for_app('man', at_least=1) 5 | def match(command): 6 | return True 7 | 8 | 9 | def get_new_command(command): 10 | if '3' in command.script: 11 | return command.script.replace("3", "2") 12 | if '2' in command.script: 13 | return command.script.replace("2", "3") 14 | 15 | last_arg = command.script_parts[-1] 16 | help_command = last_arg + ' --help' 17 | 18 | # If there are no man pages for last_arg, suggest `last_arg --help` instead. 19 | # Otherwise, suggest `--help` after suggesting other man page sections. 20 | if command.output.strip() == 'No manual entry for ' + last_arg: 21 | return [help_command] 22 | 23 | split_cmd2 = command.script_parts 24 | split_cmd3 = split_cmd2[:] 25 | 26 | split_cmd2.insert(1, ' 2 ') 27 | split_cmd3.insert(1, ' 3 ') 28 | 29 | return [ 30 | "".join(split_cmd3), 31 | "".join(split_cmd2), 32 | help_command, 33 | ] 34 | -------------------------------------------------------------------------------- /tests/rules/test_systemctl.py: -------------------------------------------------------------------------------- 1 | from thefuck.rules.systemctl import match, get_new_command 2 | from thefuck.types import Command 3 | 4 | 5 | def test_match(): 6 | assert match(Command('systemctl nginx start', 'Unknown operation \'nginx\'.')) 7 | assert match(Command('sudo systemctl nginx start', 'Unknown operation \'nginx\'.')) 8 | assert not match(Command('systemctl start nginx', '')) 9 | assert not match(Command('systemctl start nginx', '')) 10 | assert not match(Command('sudo systemctl nginx', 'Unknown operation \'nginx\'.')) 11 | assert not match(Command('systemctl nginx', 'Unknown operation \'nginx\'.')) 12 | assert not match(Command('systemctl start wtf', 'Failed to start wtf.service: Unit wtf.service failed to load: No such file or directory.')) 13 | 14 | 15 | def test_get_new_command(): 16 | assert get_new_command(Command('systemctl nginx start', '')) == "systemctl start nginx" 17 | assert get_new_command(Command('sudo systemctl nginx start', '')) == "sudo systemctl start nginx" 18 | -------------------------------------------------------------------------------- /tests/rules/test_gulp_not_task.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from io import BytesIO 3 | from thefuck.types import Command 4 | from thefuck.rules.gulp_not_task import match, get_new_command 5 | 6 | 7 | def output(task): 8 | return '''[00:41:11] Using gulpfile gulpfile.js 9 | [00:41:11] Task '{}' is not in your gulpfile 10 | [00:41:11] Please check the documentation for proper gulpfile formatting 11 | '''.format(task) 12 | 13 | 14 | def test_match(): 15 | assert match(Command('gulp srve', output('srve'))) 16 | 17 | 18 | @pytest.mark.parametrize('script, stdout', [ 19 | ('gulp serve', ''), 20 | ('cat srve', output('srve'))]) 21 | def test_not_march(script, stdout): 22 | assert not match(Command(script, stdout)) 23 | 24 | 25 | def test_get_new_command(mocker): 26 | mock = mocker.patch('subprocess.Popen') 27 | mock.return_value.stdout = BytesIO(b'serve \nbuild \ndefault \n') 28 | command = Command('gulp srve', output('srve')) 29 | assert get_new_command(command) == ['gulp serve', 'gulp default'] 30 | -------------------------------------------------------------------------------- /tests/rules/test_nixos_cmd_not_found.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.nixos_cmd_not_found import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('vim', 'nix-env -iA nixos.vim')]) 8 | def test_match(mocker, command): 9 | mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None) 10 | assert match(command) 11 | 12 | 13 | @pytest.mark.parametrize('command', [ 14 | Command('vim', ''), 15 | Command('', '')]) 16 | def test_not_match(mocker, command): 17 | mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None) 18 | assert not match(command) 19 | 20 | 21 | @pytest.mark.parametrize('command, new_command', [ 22 | (Command('vim', 'nix-env -iA nixos.vim'), 'nix-env -iA nixos.vim && vim'), 23 | (Command('pacman', 'nix-env -iA nixos.pacman'), 'nix-env -iA nixos.pacman && pacman')]) 24 | def test_get_new_command(mocker, command, new_command): 25 | assert get_new_command(command) == new_command 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | .env 60 | .idea 61 | 62 | # vim temporary files 63 | .*.swp 64 | -------------------------------------------------------------------------------- /tests/rules/test_brew_unknown_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.brew_unknown_command import match, get_new_command 3 | from thefuck.rules.brew_unknown_command import _brew_commands 4 | from thefuck.types import Command 5 | 6 | 7 | @pytest.fixture 8 | def brew_unknown_cmd(): 9 | return '''Error: Unknown command: inst''' 10 | 11 | 12 | @pytest.fixture 13 | def brew_unknown_cmd2(): 14 | return '''Error: Unknown command: instaa''' 15 | 16 | 17 | def test_match(brew_unknown_cmd): 18 | assert match(Command('brew inst', brew_unknown_cmd)) 19 | for command in _brew_commands(): 20 | assert not match(Command('brew ' + command, '')) 21 | 22 | 23 | def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2): 24 | assert (get_new_command(Command('brew inst', brew_unknown_cmd)) 25 | == ['brew list', 'brew install', 'brew uninstall']) 26 | 27 | cmds = get_new_command(Command('brew instaa', brew_unknown_cmd2)) 28 | assert 'brew install' in cmds 29 | assert 'brew uninstall' in cmds 30 | -------------------------------------------------------------------------------- /tests/rules/test_cd_mkdir.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.cd_mkdir import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('cd foo', 'cd: foo: No such file or directory'), 8 | Command('cd foo/bar/baz', 9 | 'cd: foo: No such file or directory'), 10 | Command('cd foo/bar/baz', 'cd: can\'t cd to foo/bar/baz'), 11 | Command('cd /foo/bar/', 'cd: The directory "/foo/bar/" does not exist')]) 12 | def test_match(command): 13 | assert match(command) 14 | 15 | 16 | @pytest.mark.parametrize('command', [ 17 | Command('cd foo', ''), Command('', '')]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | @pytest.mark.parametrize('command, new_command', [ 23 | (Command('cd foo', ''), 'mkdir -p foo && cd foo'), 24 | (Command('cd foo/bar/baz', ''), 'mkdir -p foo/bar/baz && cd foo/bar/baz')]) 25 | def test_get_new_command(command, new_command): 26 | assert get_new_command(command) == new_command 27 | -------------------------------------------------------------------------------- /thefuck/rules/brew_cask_dependency.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app, eager 2 | from thefuck.shells import shell 3 | from thefuck.specific.brew import brew_available 4 | 5 | 6 | @for_app('brew') 7 | def match(command): 8 | return (u'install' in command.script_parts 9 | and u'brew cask install' in command.output) 10 | 11 | 12 | @eager 13 | def _get_cask_install_lines(output): 14 | for line in output.split('\n'): 15 | line = line.strip() 16 | if line.startswith('brew cask install'): 17 | yield line 18 | 19 | 20 | def _get_script_for_brew_cask(output): 21 | cask_install_lines = _get_cask_install_lines(output) 22 | if len(cask_install_lines) > 1: 23 | return shell.and_(*cask_install_lines) 24 | else: 25 | return cask_install_lines[0] 26 | 27 | 28 | def get_new_command(command): 29 | brew_cask_script = _get_script_for_brew_cask(command.output) 30 | return shell.and_(brew_cask_script, command.script) 31 | 32 | 33 | enabled_by_default = brew_available 34 | -------------------------------------------------------------------------------- /thefuck/rules/composer_not_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import replace_argument, for_app 3 | 4 | 5 | @for_app('composer') 6 | def match(command): 7 | return (('did you mean this?' in command.output.lower() 8 | or 'did you mean one of these?' in command.output.lower())) or ( 9 | "install" in command.script_parts and "composer require" in command.output.lower() 10 | ) 11 | 12 | 13 | def get_new_command(command): 14 | if "install" in command.script_parts and "composer require" in command.output.lower(): 15 | broken_cmd, new_cmd = "install", "require" 16 | else: 17 | broken_cmd = re.findall(r"Command \"([^']*)\" is not defined", command.output)[0] 18 | new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.output) 19 | if not new_cmd: 20 | new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.output) 21 | new_cmd = new_cmd[0].strip() 22 | return replace_argument(command.script, broken_cmd, new_cmd) 23 | -------------------------------------------------------------------------------- /thefuck/rules/choco_install.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app, which 2 | 3 | 4 | @for_app("choco", "cinst") 5 | def match(command): 6 | return ((command.script.startswith('choco install') or 'cinst' in command.script_parts) 7 | and 'Installing the following packages' in command.output) 8 | 9 | 10 | def get_new_command(command): 11 | # Find the argument that is the package name 12 | for script_part in command.script_parts: 13 | if ( 14 | script_part not in ["choco", "cinst", "install"] 15 | # Need exact match (bc chocolatey is a package) 16 | and not script_part.startswith('-') 17 | # Leading hyphens are parameters; some packages contain them though 18 | and '=' not in script_part and '/' not in script_part 19 | # These are certainly parameters 20 | ): 21 | return command.script.replace(script_part, script_part + ".install") 22 | return [] 23 | 24 | 25 | enabled_by_default = bool(which("choco")) or bool(which("cinst")) 26 | -------------------------------------------------------------------------------- /tests/rules/test_git_remote_seturl_add.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_remote_seturl_add import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('git remote set-url origin url', "fatal: No such remote")]) 8 | def test_match(command): 9 | assert match(command) 10 | 11 | 12 | @pytest.mark.parametrize('command', [ 13 | Command('git remote set-url origin url', ""), 14 | Command('git remote add origin url', ''), 15 | Command('git remote remove origin', ''), 16 | Command('git remote prune origin', ''), 17 | Command('git remote set-branches origin branch', '')]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | @pytest.mark.parametrize('command, new_command', [ 23 | (Command('git remote set-url origin git@github.com:nvbn/thefuck.git', ''), 24 | 'git remote add origin git@github.com:nvbn/thefuck.git')]) 25 | def test_get_new_command(command, new_command): 26 | assert get_new_command(command) == new_command 27 | -------------------------------------------------------------------------------- /tests/rules/test_wrong_hyphen_before_subcommand.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thefuck.rules.wrong_hyphen_before_subcommand import match, get_new_command 4 | from thefuck.types import Command 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def get_all_executables(mocker): 9 | mocker.patch( 10 | "thefuck.rules.wrong_hyphen_before_subcommand.get_all_executables", 11 | return_value=["git", "apt", "apt-get", "ls", "pwd"], 12 | ) 13 | 14 | 15 | @pytest.mark.parametrize("script", ["git-log", "apt-install python"]) 16 | def test_match(script): 17 | assert match(Command(script, "")) 18 | 19 | 20 | @pytest.mark.parametrize("script", ["ls -la", "git2-make", "apt-get install python"]) 21 | def test_not_match(script): 22 | assert not match(Command(script, "")) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "script, new_command", 27 | [("git-log", "git log"), ("apt-install python", "apt install python")], 28 | ) 29 | def test_get_new_command(script, new_command): 30 | assert get_new_command(Command(script, "")) == new_command 31 | -------------------------------------------------------------------------------- /thefuck/rules/git_branch_exists.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.shells import shell 3 | from thefuck.specific.git import git_support 4 | from thefuck.utils import eager 5 | 6 | 7 | @git_support 8 | def match(command): 9 | return ("fatal: A branch named '" in command.output 10 | and "' already exists." in command.output) 11 | 12 | 13 | @git_support 14 | @eager 15 | def get_new_command(command): 16 | branch_name = re.findall( 17 | r"fatal: A branch named '(.+)' already exists.", command.output)[0] 18 | branch_name = branch_name.replace("'", r"\'") 19 | new_command_templates = [['git branch -d {0}', 'git branch {0}'], 20 | ['git branch -d {0}', 'git checkout -b {0}'], 21 | ['git branch -D {0}', 'git branch {0}'], 22 | ['git branch -D {0}', 'git checkout -b {0}'], 23 | ['git checkout {0}']] 24 | for new_command_template in new_command_templates: 25 | yield shell.and_(*new_command_template).format(branch_name) 26 | -------------------------------------------------------------------------------- /thefuck/rules/go_unknown_command.py: -------------------------------------------------------------------------------- 1 | from itertools import dropwhile, islice, takewhile 2 | import subprocess 3 | 4 | from thefuck.utils import get_closest, replace_argument, for_app, which, cache 5 | 6 | 7 | def get_golang_commands(): 8 | proc = subprocess.Popen('go', stderr=subprocess.PIPE) 9 | lines = [line.decode('utf-8').strip() for line in proc.stderr.readlines()] 10 | lines = dropwhile(lambda line: line != 'The commands are:', lines) 11 | lines = islice(lines, 2, None) 12 | lines = takewhile(lambda line: line, lines) 13 | return [line.split(' ')[0] for line in lines] 14 | 15 | 16 | if which('go'): 17 | get_golang_commands = cache(which('go'))(get_golang_commands) 18 | 19 | 20 | @for_app('go') 21 | def match(command): 22 | return 'unknown command' in command.output 23 | 24 | 25 | def get_new_command(command): 26 | closest_subcommand = get_closest(command.script_parts[1], get_golang_commands()) 27 | return replace_argument(command.script, command.script_parts[1], 28 | closest_subcommand) 29 | -------------------------------------------------------------------------------- /tests/rules/test_rm_dir.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.rm_dir import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('rm foo', 'rm: foo: is a directory'), 8 | Command('rm foo', 'rm: foo: Is a directory'), 9 | Command('hdfs dfs -rm foo', 'rm: `foo`: Is a directory'), 10 | Command('./bin/hdfs dfs -rm foo', 'rm: `foo`: Is a directory'), 11 | ]) 12 | def test_match(command): 13 | assert match(command) 14 | 15 | 16 | @pytest.mark.parametrize('command', [ 17 | Command('rm foo', ''), 18 | Command('hdfs dfs -rm foo', ''), 19 | Command('./bin/hdfs dfs -rm foo', ''), 20 | Command('', ''), 21 | ]) 22 | def test_not_match(command): 23 | assert not match(command) 24 | 25 | 26 | @pytest.mark.parametrize('command, new_command', [ 27 | (Command('rm foo', ''), 'rm -rf foo'), 28 | (Command('hdfs dfs -rm foo', ''), 'hdfs dfs -rm -r foo'), 29 | ]) 30 | def test_get_new_command(command, new_command): 31 | assert get_new_command(command) == new_command 32 | -------------------------------------------------------------------------------- /thefuck/rules/git_flag_after_filename.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.specific.git import git_support 3 | 4 | error_pattern = "fatal: bad flag '(.*?)' used after filename" 5 | error_pattern2 = "fatal: option '(.*?)' must come before non-option arguments" 6 | 7 | 8 | @git_support 9 | def match(command): 10 | return re.search(error_pattern, command.output) or re.search(error_pattern2, command.output) 11 | 12 | 13 | @git_support 14 | def get_new_command(command): 15 | command_parts = command.script_parts[:] 16 | 17 | # find the bad flag 18 | bad_flag = match(command).group(1) 19 | bad_flag_index = command_parts.index(bad_flag) 20 | 21 | # find the filename 22 | for index in reversed(range(bad_flag_index)): 23 | if command_parts[index][0] != '-': 24 | filename_index = index 25 | break 26 | 27 | # swap them 28 | command_parts[bad_flag_index], command_parts[filename_index] = \ 29 | command_parts[filename_index], command_parts[bad_flag_index] # noqa: E122 30 | 31 | return u' '.join(command_parts) 32 | -------------------------------------------------------------------------------- /thefuck/system/win32.py: -------------------------------------------------------------------------------- 1 | import os 2 | import msvcrt 3 | import win_unicode_console 4 | from .. import const 5 | 6 | 7 | def init_output(): 8 | import colorama 9 | win_unicode_console.enable() 10 | colorama.init() 11 | 12 | 13 | def get_key(): 14 | ch = msvcrt.getwch() 15 | if ch in ('\x00', '\xe0'): # arrow or function key prefix? 16 | ch = msvcrt.getwch() # second call returns the actual key code 17 | 18 | if ch in const.KEY_MAPPING: 19 | return const.KEY_MAPPING[ch] 20 | if ch == 'H': 21 | return const.KEY_UP 22 | if ch == 'P': 23 | return const.KEY_DOWN 24 | 25 | return ch 26 | 27 | 28 | def open_command(arg): 29 | return 'cmd /c start ' + arg 30 | 31 | 32 | try: 33 | from pathlib import Path 34 | except ImportError: 35 | from pathlib2 import Path 36 | 37 | 38 | def _expanduser(self): 39 | return self.__class__(os.path.expanduser(str(self))) 40 | 41 | 42 | # pathlib's expanduser fails on windows, see http://bugs.python.org/issue19776 43 | Path.expanduser = _expanduser 44 | -------------------------------------------------------------------------------- /tests/rules/test_git_branch_delete_checked_out.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_branch_delete_checked_out import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return "error: Cannot delete branch 'foo' checked out at '/bar/foo'" 9 | 10 | 11 | @pytest.mark.parametrize("script", ["git branch -d foo", "git branch -D foo"]) 12 | def test_match(script, output): 13 | assert match(Command(script, output)) 14 | 15 | 16 | @pytest.mark.parametrize("script", ["git branch -d foo", "git branch -D foo"]) 17 | def test_not_match(script): 18 | assert not match(Command(script, "Deleted branch foo (was a1b2c3d).")) 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "script, new_command", 23 | [ 24 | ("git branch -d foo", "git checkout master && git branch -D foo"), 25 | ("git branch -D foo", "git checkout master && git branch -D foo"), 26 | ], 27 | ) 28 | def test_get_new_command(script, new_command, output): 29 | assert get_new_command(Command(script, output)) == new_command 30 | -------------------------------------------------------------------------------- /thefuck/rules/react_native_command_unrecognized.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import Popen, PIPE 3 | from thefuck.utils import for_app, replace_command, cache, eager 4 | 5 | 6 | @for_app('react-native') 7 | def match(command): 8 | return re.findall(r"Unrecognized command '.*'", command.output) 9 | 10 | 11 | @cache('package.json') 12 | @eager 13 | def _get_commands(): 14 | proc = Popen(['react-native', '--help'], stdout=PIPE) 15 | should_yield = False 16 | for line in proc.stdout.readlines(): 17 | line = line.decode().strip() 18 | 19 | if not line: 20 | continue 21 | 22 | if 'Commands:' in line: 23 | should_yield = True 24 | continue 25 | 26 | if should_yield: 27 | yield line.split(' ')[0] 28 | 29 | 30 | def get_new_command(command): 31 | misspelled_command = re.findall(r"Unrecognized command '(.*)'", 32 | command.output)[0] 33 | commands = _get_commands() 34 | return replace_command(command, misspelled_command, commands) 35 | -------------------------------------------------------------------------------- /tests/rules/test_git_rebase_no_changes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_rebase_no_changes import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return '''Applying: Test commit 9 | No changes - did you forget to use 'git add'? 10 | If there is nothing left to stage, chances are that something else 11 | already introduced the same changes; you might want to skip this patch. 12 | 13 | When you have resolved this problem, run "git rebase --continue". 14 | If you prefer to skip this patch, run "git rebase --skip" instead. 15 | To check out the original branch and stop rebasing, run "git rebase --abort". 16 | 17 | ''' 18 | 19 | 20 | def test_match(output): 21 | assert match(Command('git rebase --continue', output)) 22 | assert not match(Command('git rebase --continue', '')) 23 | assert not match(Command('git rebase --skip', '')) 24 | 25 | 26 | def test_get_new_command(output): 27 | assert (get_new_command(Command('git rebase --continue', output)) == 28 | 'git rebase --skip') 29 | -------------------------------------------------------------------------------- /tests/rules/test_workon_doesnt_exists.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.workon_doesnt_exists import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def envs(mocker): 8 | return mocker.patch( 9 | 'thefuck.rules.workon_doesnt_exists._get_all_environments', 10 | return_value=['thefuck', 'code_view']) 11 | 12 | 13 | @pytest.mark.parametrize('script', [ 14 | 'workon tehfuck', 'workon code-view', 'workon new-env']) 15 | def test_match(script): 16 | assert match(Command(script, '')) 17 | 18 | 19 | @pytest.mark.parametrize('script', [ 20 | 'workon thefuck', 'workon code_view', 'work on tehfuck']) 21 | def test_not_match(script): 22 | assert not match(Command(script, '')) 23 | 24 | 25 | @pytest.mark.parametrize('script, result', [ 26 | ('workon tehfuck', 'workon thefuck'), 27 | ('workon code-view', 'workon code_view'), 28 | ('workon zzzz', 'mkvirtualenv zzzz')]) 29 | def test_get_new_command(script, result): 30 | assert get_new_command(Command(script, ''))[0] == result 31 | -------------------------------------------------------------------------------- /tests/rules/test_git_help_aliased.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_help_aliased import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('script, output', [ 7 | ('git help st', "`git st' is aliased to `status'"), 8 | ('git help ds', "`git ds' is aliased to `diff --staged'")]) 9 | def test_match(script, output): 10 | assert match(Command(script, output)) 11 | 12 | 13 | @pytest.mark.parametrize('script, output', [ 14 | ('git help status', "GIT-STATUS(1)...Git Manual...GIT-STATUS(1)"), 15 | ('git help diff', "GIT-DIFF(1)...Git Manual...GIT-DIFF(1)")]) 16 | def test_not_match(script, output): 17 | assert not match(Command(script, output)) 18 | 19 | 20 | @pytest.mark.parametrize('script, output, new_command', [ 21 | ('git help st', "`git st' is aliased to `status'", 'git help status'), 22 | ('git help ds', "`git ds' is aliased to `diff --staged'", 'git help diff')]) 23 | def test_get_new_command(script, output, new_command): 24 | assert get_new_command(Command(script, output)) == new_command 25 | -------------------------------------------------------------------------------- /thefuck/rules/git_fix_stash.py: -------------------------------------------------------------------------------- 1 | from thefuck import utils 2 | from thefuck.utils import replace_argument 3 | from thefuck.specific.git import git_support 4 | 5 | 6 | @git_support 7 | def match(command): 8 | if command.script_parts and len(command.script_parts) > 1: 9 | return (command.script_parts[1] == 'stash' 10 | and 'usage:' in command.output) 11 | else: 12 | return False 13 | 14 | 15 | # git's output here is too complicated to be parsed (see the test file) 16 | stash_commands = ( 17 | 'apply', 18 | 'branch', 19 | 'clear', 20 | 'drop', 21 | 'list', 22 | 'pop', 23 | 'save', 24 | 'show') 25 | 26 | 27 | @git_support 28 | def get_new_command(command): 29 | stash_cmd = command.script_parts[2] 30 | fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False) 31 | 32 | if fixed is not None: 33 | return replace_argument(command.script, stash_cmd, fixed) 34 | else: 35 | cmd = command.script_parts[:] 36 | cmd.insert(2, 'save') 37 | return ' '.join(cmd) 38 | -------------------------------------------------------------------------------- /tests/rules/test_whois.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.whois import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('whois https://en.wikipedia.org/wiki/Main_Page', ''), 8 | Command('whois https://en.wikipedia.org/', ''), 9 | Command('whois meta.unix.stackexchange.com', '')]) 10 | def test_match(command): 11 | assert match(command) 12 | 13 | 14 | def test_not_match(): 15 | assert not match(Command('whois', '')) 16 | 17 | 18 | # `whois com` actually makes sense 19 | @pytest.mark.parametrize('command, new_command', [ 20 | (Command('whois https://en.wikipedia.org/wiki/Main_Page', ''), 21 | 'whois en.wikipedia.org'), 22 | (Command('whois https://en.wikipedia.org/', ''), 23 | 'whois en.wikipedia.org'), 24 | (Command('whois meta.unix.stackexchange.com', ''), 25 | ['whois unix.stackexchange.com', 26 | 'whois stackexchange.com', 27 | 'whois com'])]) 28 | def test_get_new_command(command, new_command): 29 | assert get_new_command(command) == new_command 30 | -------------------------------------------------------------------------------- /tests/rules/test_missing_space_before_subcommand.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.missing_space_before_subcommand import ( 3 | match, get_new_command) 4 | from thefuck.types import Command 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def all_executables(mocker): 9 | return mocker.patch( 10 | 'thefuck.rules.missing_space_before_subcommand.get_all_executables', 11 | return_value=['git', 'ls', 'npm', 'w', 'watch']) 12 | 13 | 14 | @pytest.mark.parametrize('script', [ 15 | 'gitbranch', 'ls-la', 'npminstall', 'watchls']) 16 | def test_match(script): 17 | assert match(Command(script, '')) 18 | 19 | 20 | @pytest.mark.parametrize('script', ['git branch', 'vimfile']) 21 | def test_not_match(script): 22 | assert not match(Command(script, '')) 23 | 24 | 25 | @pytest.mark.parametrize('script, result', [ 26 | ('gitbranch', 'git branch'), 27 | ('ls-la', 'ls -la'), 28 | ('npminstall webpack', 'npm install webpack'), 29 | ('watchls', 'watch ls')]) 30 | def test_get_new_command(script, result): 31 | assert get_new_command(Command(script, '')) == result 32 | -------------------------------------------------------------------------------- /tests/entrypoints/test_fix_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mock import Mock 3 | from thefuck.entrypoints.fix_command import _get_raw_command 4 | 5 | 6 | class TestGetRawCommand(object): 7 | def test_from_force_command_argument(self): 8 | known_args = Mock(force_command='git brunch') 9 | assert _get_raw_command(known_args) == ['git brunch'] 10 | 11 | def test_from_command_argument(self, os_environ): 12 | os_environ['TF_HISTORY'] = None 13 | known_args = Mock(force_command=None, 14 | command=['sl']) 15 | assert _get_raw_command(known_args) == ['sl'] 16 | 17 | @pytest.mark.parametrize('history, result', [ 18 | ('git br', 'git br'), 19 | ('git br\nfcuk', 'git br'), 20 | ('git br\nfcuk\nls', 'ls'), 21 | ('git br\nfcuk\nls\nfuk', 'ls')]) 22 | def test_from_history(self, os_environ, history, result): 23 | os_environ['TF_HISTORY'] = history 24 | known_args = Mock(force_command=None, 25 | command=None) 26 | assert _get_raw_command(known_args) == [result] 27 | -------------------------------------------------------------------------------- /tests/rules/test_touch.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.touch import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | def output(is_bsd): 7 | if is_bsd: 8 | return "touch: /a/b/c: No such file or directory" 9 | return "touch: cannot touch '/a/b/c': No such file or directory" 10 | 11 | 12 | @pytest.mark.parametrize('script, is_bsd', [ 13 | ('touch /a/b/c', False), 14 | ('touch /a/b/c', True)]) 15 | def test_match(script, is_bsd): 16 | command = Command(script, output(is_bsd)) 17 | assert match(command) 18 | 19 | 20 | @pytest.mark.parametrize('command', [ 21 | Command('touch /a/b/c', ''), 22 | Command('ls /a/b/c', output(False))]) 23 | def test_not_match(command): 24 | assert not match(command) 25 | 26 | 27 | @pytest.mark.parametrize('script, is_bsd', [ 28 | ('touch /a/b/c', False), 29 | ('touch /a/b/c', True)]) 30 | def test_get_new_command(script, is_bsd): 31 | command = Command(script, output(is_bsd)) 32 | fixed_command = get_new_command(command) 33 | assert fixed_command == 'mkdir -p /a/b && touch /a/b/c' 34 | -------------------------------------------------------------------------------- /thefuck/rules/gradle_no_task.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import Popen, PIPE 3 | from thefuck.utils import for_app, eager, replace_command 4 | 5 | regex = re.compile(r"Task '(.*)' (is ambiguous|not found)") 6 | 7 | 8 | @for_app('gradle', 'gradlew') 9 | def match(command): 10 | return regex.findall(command.output) 11 | 12 | 13 | @eager 14 | def _get_all_tasks(gradle): 15 | proc = Popen([gradle, 'tasks'], stdout=PIPE) 16 | should_yield = False 17 | for line in proc.stdout.readlines(): 18 | line = line.decode().strip() 19 | if line.startswith('----'): 20 | should_yield = True 21 | continue 22 | 23 | if not line.strip(): 24 | should_yield = False 25 | continue 26 | 27 | if should_yield and not line.startswith('All tasks runnable from root project'): 28 | yield line.split(' ')[0] 29 | 30 | 31 | def get_new_command(command): 32 | wrong_task = regex.findall(command.output)[0][0] 33 | all_tasks = _get_all_tasks(command.script_parts[0]) 34 | return replace_command(command, wrong_task, all_tasks) 35 | -------------------------------------------------------------------------------- /tests/rules/test_no_such_file.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.no_such_file import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('mv foo bar/foo', "mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 8 | Command('mv foo bar/', "mv: cannot move 'foo' to 'bar/': No such file or directory"), 9 | ]) 10 | def test_match(command): 11 | assert match(command) 12 | 13 | 14 | @pytest.mark.parametrize('command', [ 15 | Command('mv foo bar/', ""), 16 | Command('mv foo bar/foo', "mv: permission denied"), 17 | ]) 18 | def test_not_match(command): 19 | assert not match(command) 20 | 21 | 22 | @pytest.mark.parametrize('command, new_command', [ 23 | (Command('mv foo bar/foo', "mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'), 24 | (Command('mv foo bar/', "mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'), 25 | ]) 26 | def test_get_new_command(command, new_command): 27 | assert get_new_command(command) == new_command 28 | -------------------------------------------------------------------------------- /tests/rules/test_brew_cask_dependency.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.brew_cask_dependency import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | output = '''sshfs: OsxfuseRequirement unsatisfied! 7 | 8 | You can install with Homebrew-Cask: 9 | brew cask install osxfuse 10 | 11 | You can download from: 12 | https://osxfuse.github.io/ 13 | Error: An unsatisfied requirement failed this build.''' 14 | 15 | 16 | def test_match(): 17 | command = Command('brew install sshfs', output) 18 | assert match(command) 19 | 20 | 21 | @pytest.mark.parametrize('script, output', [ 22 | ('brew link sshfs', output), 23 | ('cat output', output), 24 | ('brew install sshfs', '')]) 25 | def test_not_match(script, output): 26 | command = Command(script, output) 27 | assert not match(command) 28 | 29 | 30 | @pytest.mark.parametrize('before, after', [ 31 | ('brew install sshfs', 32 | 'brew cask install osxfuse && brew install sshfs')]) 33 | def test_get_new_command(before, after): 34 | command = Command(before, output) 35 | assert get_new_command(command) == after 36 | -------------------------------------------------------------------------------- /tests/rules/test_git_rm_staged.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_rm_staged import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(target): 8 | return ('error: the following file has changes staged in the index:\n {}\n(use ' 9 | '--cached to keep the file, or -f to force removal)').format(target) 10 | 11 | 12 | @pytest.mark.parametrize('script, target', [ 13 | ('git rm foo', 'foo'), 14 | ('git rm foo bar', 'bar')]) 15 | def test_match(output, script, target): 16 | assert match(Command(script, output)) 17 | 18 | 19 | @pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar', 'git rm']) 20 | def test_not_match(script): 21 | assert not match(Command(script, '')) 22 | 23 | 24 | @pytest.mark.parametrize('script, target, new_command', [ 25 | ('git rm foo', 'foo', ['git rm --cached foo', 'git rm -f foo']), 26 | ('git rm foo bar', 'bar', ['git rm --cached foo bar', 'git rm -f foo bar'])]) 27 | def test_get_new_command(output, script, target, new_command): 28 | assert get_new_command(Command(script, output)) == new_command 29 | -------------------------------------------------------------------------------- /tests/functional/test_fish.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.functional.plots import with_confirmation, without_confirmation, \ 3 | refuse_with_confirmation, select_command_with_arrows 4 | 5 | containers = ((u'thefuck/python3', u'', u'fish'), 6 | (u'thefuck/python2', u'', u'fish')) 7 | 8 | 9 | @pytest.fixture(params=containers) 10 | def proc(request, spawnu, TIMEOUT): 11 | proc = spawnu(*request.param) 12 | proc.sendline(u'thefuck --alias > ~/.config/fish/config.fish') 13 | proc.sendline(u'fish') 14 | return proc 15 | 16 | 17 | @pytest.mark.functional 18 | def test_with_confirmation(proc, TIMEOUT): 19 | with_confirmation(proc, TIMEOUT) 20 | 21 | 22 | @pytest.mark.functional 23 | def test_select_command_with_arrows(proc, TIMEOUT): 24 | select_command_with_arrows(proc, TIMEOUT) 25 | 26 | 27 | @pytest.mark.functional 28 | def test_refuse_with_confirmation(proc, TIMEOUT): 29 | refuse_with_confirmation(proc, TIMEOUT) 30 | 31 | 32 | @pytest.mark.functional 33 | def test_without_confirmation(proc, TIMEOUT): 34 | without_confirmation(proc, TIMEOUT) 35 | 36 | # TODO: ensure that history changes. 37 | -------------------------------------------------------------------------------- /tests/rules/test_git_bisect_usage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.types import Command 3 | from thefuck.rules.git_bisect_usage import match, get_new_command 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return ("usage: git bisect [help|start|bad|good|new|old" 9 | "|terms|skip|next|reset|visualize|replay|log|run]") 10 | 11 | 12 | @pytest.mark.parametrize('script', [ 13 | 'git bisect strt', 'git bisect rset', 'git bisect goood']) 14 | def test_match(output, script): 15 | assert match(Command(script, output)) 16 | 17 | 18 | @pytest.mark.parametrize('script', [ 19 | 'git bisect', 'git bisect start', 'git bisect good']) 20 | def test_not_match(script): 21 | assert not match(Command(script, '')) 22 | 23 | 24 | @pytest.mark.parametrize('script, new_cmd, ', [ 25 | ('git bisect goood', ['good', 'old', 'log']), 26 | ('git bisect strt', ['start', 'terms', 'reset']), 27 | ('git bisect rset', ['reset', 'next', 'start'])]) 28 | def test_get_new_command(output, script, new_cmd): 29 | new_cmd = ['git bisect %s' % cmd for cmd in new_cmd] 30 | assert get_new_command(Command(script, output)) == new_cmd 31 | -------------------------------------------------------------------------------- /thefuck/rules/gem_unknown_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | from thefuck.utils import for_app, eager, replace_command, cache, which 4 | 5 | 6 | @for_app('gem') 7 | def match(command): 8 | return ('ERROR: While executing gem ... (Gem::CommandLineError)' 9 | in command.output 10 | and 'Unknown command' in command.output) 11 | 12 | 13 | def _get_unknown_command(command): 14 | return re.findall(r'Unknown command (.*)$', command.output)[0] 15 | 16 | 17 | @eager 18 | def _get_all_commands(): 19 | proc = subprocess.Popen(['gem', 'help', 'commands'], 20 | stdout=subprocess.PIPE) 21 | 22 | for line in proc.stdout.readlines(): 23 | line = line.decode() 24 | 25 | if line.startswith(' '): 26 | yield line.strip().split(' ')[0] 27 | 28 | 29 | if which('gem'): 30 | _get_all_commands = cache(which('gem'))(_get_all_commands) 31 | 32 | 33 | def get_new_command(command): 34 | unknown_command = _get_unknown_command(command) 35 | all_commands = _get_all_commands() 36 | return replace_command(command, unknown_command, all_commands) 37 | -------------------------------------------------------------------------------- /thefuck/rules/dnf_no_such_command.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import re 3 | from thefuck.specific.sudo import sudo_support 4 | from thefuck.utils import for_app, replace_command 5 | from thefuck.specific.dnf import dnf_available 6 | 7 | 8 | regex = re.compile(r'No such command: (.*)\.') 9 | 10 | 11 | @sudo_support 12 | @for_app('dnf') 13 | def match(command): 14 | return 'no such command' in command.output.lower() 15 | 16 | 17 | def _parse_operations(help_text_lines): 18 | operation_regex = re.compile(r'^([a-z-]+) +', re.MULTILINE) 19 | return operation_regex.findall(help_text_lines) 20 | 21 | 22 | def _get_operations(): 23 | proc = subprocess.Popen(["dnf", '--help'], 24 | stdout=subprocess.PIPE, 25 | stderr=subprocess.PIPE) 26 | lines = proc.stdout.read().decode("utf-8") 27 | 28 | return _parse_operations(lines) 29 | 30 | 31 | @sudo_support 32 | def get_new_command(command): 33 | misspelled_command = regex.findall(command.output)[0] 34 | return replace_command(command, misspelled_command, _get_operations()) 35 | 36 | 37 | enabled_by_default = dnf_available 38 | -------------------------------------------------------------------------------- /thefuck/rules/grunt_task_not_found.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import Popen, PIPE 3 | from thefuck.utils import for_app, eager, get_closest, cache 4 | 5 | regex = re.compile(r'Warning: Task "(.*)" not found.') 6 | 7 | 8 | @for_app('grunt') 9 | def match(command): 10 | return regex.findall(command.output) 11 | 12 | 13 | @cache('Gruntfile.js') 14 | @eager 15 | def _get_all_tasks(): 16 | proc = Popen(['grunt', '--help'], stdout=PIPE) 17 | should_yield = False 18 | for line in proc.stdout.readlines(): 19 | line = line.decode().strip() 20 | 21 | if 'Available tasks' in line: 22 | should_yield = True 23 | continue 24 | 25 | if should_yield and not line: 26 | return 27 | 28 | if ' ' in line: 29 | yield line.split(' ')[0] 30 | 31 | 32 | def get_new_command(command): 33 | misspelled_task = regex.findall(command.output)[0].split(':')[0] 34 | tasks = _get_all_tasks() 35 | fixed = get_closest(misspelled_task, tasks) 36 | return command.script.replace(' {}'.format(misspelled_task), 37 | ' {}'.format(fixed)) 38 | -------------------------------------------------------------------------------- /tests/rules/test_git_rm_local_modifications.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_rm_local_modifications import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def output(target): 8 | return ('error: the following file has local modifications:\n {}\n(use ' 9 | '--cached to keep the file, or -f to force removal)').format(target) 10 | 11 | 12 | @pytest.mark.parametrize('script, target', [ 13 | ('git rm foo', 'foo'), 14 | ('git rm foo bar', 'bar')]) 15 | def test_match(output, script, target): 16 | assert match(Command(script, output)) 17 | 18 | 19 | @pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar', 'git rm']) 20 | def test_not_match(script): 21 | assert not match(Command(script, '')) 22 | 23 | 24 | @pytest.mark.parametrize('script, target, new_command', [ 25 | ('git rm foo', 'foo', ['git rm --cached foo', 'git rm -f foo']), 26 | ('git rm foo bar', 'bar', ['git rm --cached foo bar', 'git rm -f foo bar'])]) 27 | def test_get_new_command(output, script, target, new_command): 28 | assert get_new_command(Command(script, output)) == new_command 29 | -------------------------------------------------------------------------------- /tests/rules/test_pacman_invalid_option.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.pacman_invalid_option import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | good_output = """community/shared_meataxe 1.0-3 6 | A set of programs for working with matrix representations over finite fields 7 | """ 8 | 9 | bad_output = "error: invalid option '-" 10 | 11 | 12 | @pytest.mark.parametrize("option", "SURQFDVT") 13 | def test_not_match_good_output(option): 14 | assert not match(Command("pacman -{}s meat".format(option), good_output)) 15 | 16 | 17 | @pytest.mark.parametrize("option", "azxcbnm") 18 | def test_not_match_bad_output(option): 19 | assert not match(Command("pacman -{}v meat".format(option), bad_output)) 20 | 21 | 22 | @pytest.mark.parametrize("option", "surqfdvt") 23 | def test_match(option): 24 | assert match(Command("pacman -{}v meat".format(option), bad_output)) 25 | 26 | 27 | @pytest.mark.parametrize("option", "surqfdvt") 28 | def test_get_new_command(option): 29 | new_command = get_new_command(Command("pacman -{}v meat".format(option), "")) 30 | assert new_command == "pacman -{}v meat".format(option.upper()) 31 | -------------------------------------------------------------------------------- /thefuck/rules/mvn_unknown_lifecycle_phase.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import for_app, get_close_matches, replace_command 2 | import re 3 | 4 | 5 | def _get_failed_lifecycle(command): 6 | return re.search(r'\[ERROR\] Unknown lifecycle phase "(.+)"', 7 | command.output) 8 | 9 | 10 | def _getavailable_lifecycles(command): 11 | return re.search( 12 | r'Available lifecycle phases are: (.+) -> \[Help 1\]', command.output) 13 | 14 | 15 | @for_app('mvn') 16 | def match(command): 17 | failed_lifecycle = _get_failed_lifecycle(command) 18 | available_lifecycles = _getavailable_lifecycles(command) 19 | return available_lifecycles and failed_lifecycle 20 | 21 | 22 | def get_new_command(command): 23 | failed_lifecycle = _get_failed_lifecycle(command) 24 | available_lifecycles = _getavailable_lifecycles(command) 25 | if available_lifecycles and failed_lifecycle: 26 | selected_lifecycle = get_close_matches( 27 | failed_lifecycle.group(1), available_lifecycles.group(1).split(", ")) 28 | return replace_command(command, failed_lifecycle.group(1), selected_lifecycle) 29 | else: 30 | return [] 31 | -------------------------------------------------------------------------------- /tests/functional/test_tcsh.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.functional.plots import with_confirmation, without_confirmation, \ 3 | refuse_with_confirmation, select_command_with_arrows 4 | 5 | containers = ((u'thefuck/python3', u'', u'tcsh'), 6 | (u'thefuck/python2', u'', u'tcsh')) 7 | 8 | 9 | @pytest.fixture(params=containers) 10 | def proc(request, spawnu, TIMEOUT): 11 | proc = spawnu(*request.param) 12 | proc.sendline(u'tcsh') 13 | proc.sendline(u'setenv PYTHONIOENCODING utf8') 14 | proc.sendline(u'eval `thefuck --alias`') 15 | return proc 16 | 17 | 18 | @pytest.mark.functional 19 | def test_with_confirmation(proc, TIMEOUT): 20 | with_confirmation(proc, TIMEOUT) 21 | 22 | 23 | @pytest.mark.functional 24 | def test_select_command_with_arrows(proc, TIMEOUT): 25 | select_command_with_arrows(proc, TIMEOUT) 26 | 27 | 28 | @pytest.mark.functional 29 | def test_refuse_with_confirmation(proc, TIMEOUT): 30 | refuse_with_confirmation(proc, TIMEOUT) 31 | 32 | 33 | @pytest.mark.functional 34 | def test_without_confirmation(proc, TIMEOUT): 35 | without_confirmation(proc, TIMEOUT) 36 | 37 | # TODO: ensure that history changes. 38 | -------------------------------------------------------------------------------- /tests/rules/test_cp_create_destination.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.cp_create_destination import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "script, output", 8 | [("cp", "cp: directory foo does not exist\n"), ("mv", "No such file or directory")], 9 | ) 10 | def test_match(script, output): 11 | assert match(Command(script, output)) 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "script, output", [("cp", ""), ("mv", ""), ("ls", "No such file or directory")] 16 | ) 17 | def test_not_match(script, output): 18 | assert not match(Command(script, output)) 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "script, output, new_command", 23 | [ 24 | ("cp foo bar/", "cp: directory foo does not exist\n", "mkdir -p bar/ && cp foo bar/"), 25 | ("mv foo bar/", "No such file or directory", "mkdir -p bar/ && mv foo bar/"), 26 | ("cp foo bar/baz/", "cp: directory foo does not exist\n", "mkdir -p bar/baz/ && cp foo bar/baz/"), 27 | ], 28 | ) 29 | def test_get_new_command(script, output, new_command): 30 | assert get_new_command(Command(script, output)) == new_command 31 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from subprocess import call 3 | import os 4 | import re 5 | 6 | 7 | version = None 8 | 9 | 10 | def get_new_setup_py_lines(): 11 | global version 12 | with open('setup.py', 'r') as sf: 13 | current_setup = sf.readlines() 14 | for line in current_setup: 15 | if line.startswith('VERSION = '): 16 | major, minor = re.findall(r"VERSION = '(\d+)\.(\d+)'", line)[0] 17 | version = "{}.{}".format(major, int(minor) + 1) 18 | yield "VERSION = '{}'\n".format(version) 19 | else: 20 | yield line 21 | 22 | 23 | lines = list(get_new_setup_py_lines()) 24 | with open('setup.py', 'w') as sf: 25 | sf.writelines(lines) 26 | 27 | call('git pull', shell=True) 28 | call('git commit -am "Bump to {}"'.format(version), shell=True) 29 | call('git tag {}'.format(version), shell=True) 30 | call('git push', shell=True) 31 | call('git push --tags', shell=True) 32 | 33 | env = os.environ 34 | env['CONVERT_README'] = 'true' 35 | call('rm -rf dist/*', shell=True, env=env) 36 | call('python setup.py sdist bdist_wheel', shell=True, env=env) 37 | call('twine upload dist/*', shell=True, env=env) 38 | -------------------------------------------------------------------------------- /tests/rules/test_yarn_command_replaced.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.types import Command 3 | from thefuck.rules.yarn_command_replaced import match, get_new_command 4 | 5 | 6 | output = ('error `install` has been replaced with `add` to add new ' 7 | 'dependencies. Run "yarn add {}" instead.').format 8 | 9 | 10 | @pytest.mark.parametrize('command', [ 11 | Command('yarn install redux', output('redux')), 12 | Command('yarn install moment', output('moment')), 13 | Command('yarn install lodash', output('lodash'))]) 14 | def test_match(command): 15 | assert match(command) 16 | 17 | 18 | @pytest.mark.parametrize('command', [ 19 | Command('yarn install', '')]) 20 | def test_not_match(command): 21 | assert not match(command) 22 | 23 | 24 | @pytest.mark.parametrize('command, new_command', [ 25 | (Command('yarn install redux', output('redux')), 26 | 'yarn add redux'), 27 | (Command('yarn install moment', output('moment')), 28 | 'yarn add moment'), 29 | (Command('yarn install lodash', output('lodash')), 30 | 'yarn add lodash')]) 31 | def test_get_new_command(command, new_command): 32 | assert get_new_command(command) == new_command 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015-2022 Vladimir Iakovlev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/rules/test_brew_uninstall.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.types import Command 3 | from thefuck.rules.brew_uninstall import get_new_command, match 4 | 5 | 6 | @pytest.fixture 7 | def output(): 8 | return ("Uninstalling /usr/local/Cellar/tbb/4.4-20160916... (118 files, 1.9M)\n" 9 | "tbb 4.4-20160526, 4.4-20160722 are still installed.\n" 10 | "Remove all versions with `brew uninstall --force tbb`.\n") 11 | 12 | 13 | @pytest.fixture 14 | def new_command(formula): 15 | return 'brew uninstall --force {}'.format(formula) 16 | 17 | 18 | @pytest.mark.parametrize('script', ['brew uninstall tbb', 'brew rm tbb', 'brew remove tbb']) 19 | def test_match(output, script): 20 | assert match(Command(script, output)) 21 | 22 | 23 | @pytest.mark.parametrize('script', ['brew remove gnuplot']) 24 | def test_not_match(script): 25 | output = 'Uninstalling /usr/local/Cellar/gnuplot/5.0.4_1... (44 files, 2.3M)\n' 26 | assert not match(Command(script, output)) 27 | 28 | 29 | @pytest.mark.parametrize('script, formula, ', [('brew uninstall tbb', 'tbb')]) 30 | def test_get_new_command(output, new_command, script, formula): 31 | assert get_new_command(Command(script, output)) == new_command 32 | -------------------------------------------------------------------------------- /tests/rules/test_git_commit_add.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_commit_add import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "script, output", 8 | [ 9 | ('git commit -m "test"', "no changes added to commit"), 10 | ("git commit", "no changes added to commit"), 11 | ], 12 | ) 13 | def test_match(output, script): 14 | assert match(Command(script, output)) 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "script, output", 19 | [ 20 | ('git commit -m "test"', " 1 file changed, 15 insertions(+), 14 deletions(-)"), 21 | ("git branch foo", ""), 22 | ("git checkout feature/test_commit", ""), 23 | ("git push", ""), 24 | ], 25 | ) 26 | def test_not_match(output, script): 27 | assert not match(Command(script, output)) 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "script, new_command", 32 | [ 33 | ("git commit", ["git commit -a", "git commit -p"]), 34 | ('git commit -m "foo"', ['git commit -a -m "foo"', 'git commit -p -m "foo"']), 35 | ], 36 | ) 37 | def test_get_new_command(script, new_command): 38 | assert get_new_command(Command(script, "")) == new_command 39 | -------------------------------------------------------------------------------- /thefuck/rules/fab_command_not_found.py: -------------------------------------------------------------------------------- 1 | from thefuck.utils import eager, get_closest, for_app 2 | 3 | 4 | @for_app('fab') 5 | def match(command): 6 | return 'Warning: Command(s) not found:' in command.output 7 | 8 | 9 | # We need different behavior then in get_all_matched_commands. 10 | @eager 11 | def _get_between(content, start, end=None): 12 | should_yield = False 13 | for line in content.split('\n'): 14 | if start in line: 15 | should_yield = True 16 | continue 17 | 18 | if end and end in line: 19 | return 20 | 21 | if should_yield and line: 22 | yield line.strip().split(' ')[0] 23 | 24 | 25 | def get_new_command(command): 26 | not_found_commands = _get_between( 27 | command.output, 'Warning: Command(s) not found:', 28 | 'Available commands:') 29 | possible_commands = _get_between( 30 | command.output, 'Available commands:') 31 | 32 | script = command.script 33 | for not_found in not_found_commands: 34 | fix = get_closest(not_found, possible_commands) 35 | script = script.replace(' {}'.format(not_found), 36 | ' {}'.format(fix)) 37 | 38 | return script 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | The output of `thefuck --version` (something like `The Fuck 3.1 using Python 10 | 3.5.0 and Bash 4.4.12(1)-release`): 11 | 12 | FILL THIS IN 13 | 14 | Your system (Debian 7, ArchLinux, Windows, etc.): 15 | 16 | FILL THIS IN 17 | 18 | How to reproduce the bug: 19 | 20 | FILL THIS IN 21 | 22 | The output of The Fuck with `THEFUCK_DEBUG=true` exported (typically execute `export THEFUCK_DEBUG=true` in your shell before The Fuck): 23 | 24 | FILL THIS IN 25 | 26 | If the bug only appears with a specific application, the output of that application and its version: 27 | 28 | FILL THIS IN 29 | 30 | Anything else you think is relevant: 31 | 32 | FILL THIS IN 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/rules/test_git_push_different_branch_names.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_push_different_branch_names import get_new_command, match 3 | from thefuck.types import Command 4 | 5 | 6 | output = """fatal: The upstream branch of your current branch does not match 7 | the name of your current branch. To push to the upstream branch 8 | on the remote, use 9 | 10 | git push origin HEAD:%s 11 | 12 | To push to the branch of the same name on the remote, use 13 | 14 | git push origin %s 15 | 16 | To choose either option permanently, see push.default in 'git help config'. 17 | """ 18 | 19 | 20 | def error_msg(localbranch, remotebranch): 21 | return output % (remotebranch, localbranch) 22 | 23 | 24 | def test_match(): 25 | assert match(Command('git push', error_msg('foo', 'bar'))) 26 | 27 | 28 | @pytest.mark.parametrize('command', [ 29 | Command('vim', ''), 30 | Command('git status', error_msg('foo', 'bar')), 31 | Command('git push', '') 32 | ]) 33 | def test_not_match(command): 34 | assert not match(command) 35 | 36 | 37 | def test_get_new_command(): 38 | new_command = get_new_command(Command('git push', error_msg('foo', 'bar'))) 39 | assert new_command == 'git push origin HEAD:bar' 40 | -------------------------------------------------------------------------------- /thefuck/rules/omnienv_no_such_command.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import (cache, for_app, replace_argument, replace_command, 3 | which) 4 | from subprocess import PIPE, Popen 5 | 6 | 7 | supported_apps = 'goenv', 'nodenv', 'pyenv', 'rbenv' 8 | enabled_by_default = any(which(a) for a in supported_apps) 9 | 10 | 11 | COMMON_TYPOS = { 12 | 'list': ['versions', 'install --list'], 13 | 'remove': ['uninstall'], 14 | } 15 | 16 | 17 | @for_app(*supported_apps, at_least=1) 18 | def match(command): 19 | return 'env: no such command ' in command.output 20 | 21 | 22 | def get_app_commands(app): 23 | proc = Popen([app, 'commands'], stdout=PIPE) 24 | return [line.decode('utf-8').strip() for line in proc.stdout.readlines()] 25 | 26 | 27 | def get_new_command(command): 28 | broken = re.findall(r"env: no such command ['`]([^']*)'", command.output)[0] 29 | matched = [replace_argument(command.script, broken, common_typo) 30 | for common_typo in COMMON_TYPOS.get(broken, [])] 31 | 32 | app = command.script_parts[0] 33 | app_commands = cache(which(app))(get_app_commands)(app) 34 | matched.extend(replace_command(command, broken, app_commands)) 35 | return matched 36 | -------------------------------------------------------------------------------- /tests/rules/test_ln_s_order.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.ln_s_order import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def file_exists(mocker): 8 | return mocker.patch('os.path.exists', return_value=True) 9 | 10 | 11 | get_output = "ln: failed to create symbolic link '{}': File exists".format 12 | 13 | 14 | @pytest.mark.parametrize('script, output, exists', [ 15 | ('ln dest source', get_output('source'), True), 16 | ('ls -s dest source', get_output('source'), True), 17 | ('ln -s dest source', '', True), 18 | ('ln -s dest source', get_output('source'), False)]) 19 | def test_not_match(file_exists, script, output, exists): 20 | file_exists.return_value = exists 21 | assert not match(Command(script, output)) 22 | 23 | 24 | @pytest.mark.usefixtures('file_exists') 25 | @pytest.mark.parametrize('script, result', [ 26 | ('ln -s dest source', 'ln -s source dest'), 27 | ('ln dest -s source', 'ln -s source dest'), 28 | ('ln dest source -s', 'ln source -s dest')]) 29 | def test_match(script, result): 30 | output = get_output('source') 31 | assert match(Command(script, output)) 32 | assert get_new_command(Command(script, output)) == result 33 | -------------------------------------------------------------------------------- /thefuck/rules/ssh_known_hosts.py: -------------------------------------------------------------------------------- 1 | import re 2 | from thefuck.utils import for_app 3 | 4 | commands = ('ssh', 'scp') 5 | 6 | 7 | @for_app(*commands) 8 | def match(command): 9 | if not command.script: 10 | return False 11 | if not command.script.startswith(commands): 12 | return False 13 | 14 | patterns = ( 15 | r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!', 16 | r'WARNING: POSSIBLE DNS SPOOFING DETECTED!', 17 | r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'", 18 | ) 19 | 20 | return any(re.findall(pattern, command.output) for pattern in patterns) 21 | 22 | 23 | def get_new_command(command): 24 | return command.script 25 | 26 | 27 | def side_effect(old_cmd, command): 28 | offending_pattern = re.compile( 29 | r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)', 30 | re.MULTILINE) 31 | offending = offending_pattern.findall(old_cmd.output) 32 | for filepath, lineno in offending: 33 | with open(filepath, 'r') as fh: 34 | lines = fh.readlines() 35 | del lines[int(lineno) - 1] 36 | with open(filepath, 'w') as fh: 37 | fh.writelines(lines) 38 | -------------------------------------------------------------------------------- /tests/rules/test_tsuru_login.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.tsuru_login import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | error_msg = ( 7 | "Error: you're not authenticated or your session has expired.", 8 | ("You're not authenticated or your session has expired. " 9 | "Please use \"login\" command for authentication."), 10 | ) 11 | 12 | 13 | @pytest.mark.parametrize('command', [ 14 | Command('tsuru app-shell', error_msg[0]), 15 | Command('tsuru app-log -f', error_msg[1]), 16 | ]) 17 | def test_match(command): 18 | assert match(command) 19 | 20 | 21 | @pytest.mark.parametrize('command', [ 22 | Command('tsuru', ''), 23 | Command('tsuru app-restart', 'Error: unauthorized'), 24 | Command('tsuru app-log -f', 'Error: unparseable data'), 25 | ]) 26 | def test_not_match(command): 27 | assert not match(command) 28 | 29 | 30 | @pytest.mark.parametrize('command, new_command', [ 31 | (Command('tsuru app-shell', error_msg[0]), 32 | 'tsuru login && tsuru app-shell'), 33 | (Command('tsuru app-log -f', error_msg[1]), 34 | 'tsuru login && tsuru app-log -f'), 35 | ]) 36 | def test_get_new_command(command, new_command): 37 | assert get_new_command(command) == new_command 38 | -------------------------------------------------------------------------------- /thefuck/rules/port_already_in_use.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import Popen, PIPE 3 | from thefuck.utils import memoize, which 4 | from thefuck.shells import shell 5 | 6 | enabled_by_default = bool(which('lsof')) 7 | 8 | patterns = [r"bind on address \('.*', (?P\d+)\)", 9 | r'Unable to bind [^ ]*:(?P\d+)', 10 | r"can't listen on port (?P\d+)", 11 | r'listen EADDRINUSE [^ ]*:(?P\d+)'] 12 | 13 | 14 | @memoize 15 | def _get_pid_by_port(port): 16 | proc = Popen(['lsof', '-i', ':{}'.format(port)], stdout=PIPE) 17 | lines = proc.stdout.read().decode().split('\n') 18 | if len(lines) > 1: 19 | return lines[1].split()[1] 20 | else: 21 | return None 22 | 23 | 24 | @memoize 25 | def _get_used_port(command): 26 | for pattern in patterns: 27 | matched = re.search(pattern, command.output) 28 | if matched: 29 | return matched.group('port') 30 | 31 | 32 | def match(command): 33 | port = _get_used_port(command) 34 | return port and _get_pid_by_port(port) 35 | 36 | 37 | def get_new_command(command): 38 | port = _get_used_port(command) 39 | pid = _get_pid_by_port(port) 40 | return shell.and_(u'kill {}'.format(pid), command.script) 41 | -------------------------------------------------------------------------------- /tests/rules/test_pip_unknown_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.pip_unknown_command import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def pip_unknown_cmd_without_recommend(): 8 | return '''ERROR: unknown command "i"''' 9 | 10 | 11 | @pytest.fixture 12 | def broken(): 13 | return 'instatl' 14 | 15 | 16 | @pytest.fixture 17 | def suggested(): 18 | return 'install' 19 | 20 | 21 | @pytest.fixture 22 | def pip_unknown_cmd(broken, suggested): 23 | return 'ERROR: unknown command "{}" - maybe you meant "{}"'.format(broken, suggested) 24 | 25 | 26 | def test_match(pip_unknown_cmd, pip_unknown_cmd_without_recommend): 27 | assert match(Command('pip instatl', pip_unknown_cmd)) 28 | assert not match(Command('pip i', 29 | pip_unknown_cmd_without_recommend)) 30 | 31 | 32 | @pytest.mark.parametrize('script, broken, suggested, new_cmd', [ 33 | ('pip un+install thefuck', 'un+install', 'uninstall', 'pip uninstall thefuck'), 34 | ('pip instatl', 'instatl', 'install', 'pip install')]) 35 | def test_get_new_command(script, new_cmd, pip_unknown_cmd): 36 | assert get_new_command(Command(script, 37 | pip_unknown_cmd)) == new_cmd 38 | -------------------------------------------------------------------------------- /tests/rules/test_git_fix_stash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_fix_stash import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | git_stash_err = ''' 7 | usage: git stash list [] 8 | or: git stash show [] 9 | or: git stash drop [-q|--quiet] [] 10 | or: git stash ( pop | apply ) [--index] [-q|--quiet] [] 11 | or: git stash branch [] 12 | or: git stash [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] 13 | \t\t [-u|--include-untracked] [-a|--all] []] 14 | or: git stash clear 15 | ''' 16 | 17 | 18 | @pytest.mark.parametrize('wrong', [ 19 | 'git stash opp', 20 | 'git stash Some message', 21 | 'git stash saev Some message']) 22 | def test_match(wrong): 23 | assert match(Command(wrong, git_stash_err)) 24 | 25 | 26 | def test_not_match(): 27 | assert not match(Command("git", git_stash_err)) 28 | 29 | 30 | @pytest.mark.parametrize('wrong,fixed', [ 31 | ('git stash opp', 'git stash pop'), 32 | ('git stash Some message', 'git stash save Some message'), 33 | ('git stash saev Some message', 'git stash save Some message')]) 34 | def test_get_new_command(wrong, fixed): 35 | assert get_new_command(Command(wrong, git_stash_err)) == fixed 36 | -------------------------------------------------------------------------------- /thefuck/specific/git.py: -------------------------------------------------------------------------------- 1 | import re 2 | from decorator import decorator 3 | from ..utils import is_app 4 | from ..shells import shell 5 | 6 | 7 | @decorator 8 | def git_support(fn, command): 9 | """Resolves git aliases and supports testing for both git and hub.""" 10 | # supports GitHub's `hub` command 11 | # which is recommended to be used with `alias git=hub` 12 | # but at this point, shell aliases have already been resolved 13 | if not is_app(command, 'git', 'hub'): 14 | return False 15 | 16 | # perform git aliases expansion 17 | if command.output and 'trace: alias expansion:' in command.output: 18 | search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", 19 | command.output) 20 | alias = search.group(1) 21 | 22 | # by default git quotes everything, for example: 23 | # 'commit' '--amend' 24 | # which is surprising and does not allow to easily test for 25 | # eg. 'git commit' 26 | expansion = ' '.join(shell.quote(part) 27 | for part in shell.split_command(search.group(2))) 28 | new_script = re.sub(r"\b{}\b".format(alias), expansion, command.script) 29 | 30 | command = command.update(script=new_script) 31 | 32 | return fn(command) 33 | -------------------------------------------------------------------------------- /tests/rules/test_sed_unterminated_s.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.sed_unterminated_s import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.fixture 7 | def sed_unterminated_s(): 8 | return "sed: -e expression #1, char 9: unterminated `s' command" 9 | 10 | 11 | def test_match(sed_unterminated_s): 12 | assert match(Command('sed -e s/foo/bar', sed_unterminated_s)) 13 | assert match(Command('sed -es/foo/bar', sed_unterminated_s)) 14 | assert match(Command('sed -e s/foo/bar -e s/baz/quz', sed_unterminated_s)) 15 | assert not match(Command('sed -e s/foo/bar', '')) 16 | assert not match(Command('sed -es/foo/bar', '')) 17 | assert not match(Command('sed -e s/foo/bar -e s/baz/quz', '')) 18 | 19 | 20 | def test_get_new_command(sed_unterminated_s): 21 | assert (get_new_command(Command('sed -e s/foo/bar', sed_unterminated_s)) 22 | == 'sed -e s/foo/bar/') 23 | assert (get_new_command(Command('sed -es/foo/bar', sed_unterminated_s)) 24 | == 'sed -es/foo/bar/') 25 | assert (get_new_command(Command(r"sed -e 's/\/foo/bar'", sed_unterminated_s)) 26 | == r"sed -e 's/\/foo/bar/'") 27 | assert (get_new_command(Command(r"sed -e s/foo/bar -es/baz/quz", sed_unterminated_s)) 28 | == r"sed -e s/foo/bar/ -es/baz/quz/") 29 | -------------------------------------------------------------------------------- /thefuck/rules/yum_invalid_operation.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from itertools import dropwhile, islice, takewhile 3 | 4 | from thefuck.specific.sudo import sudo_support 5 | from thefuck.specific.yum import yum_available 6 | from thefuck.utils import for_app, replace_command, which, cache 7 | 8 | enabled_by_default = yum_available 9 | 10 | 11 | @sudo_support 12 | @for_app('yum') 13 | def match(command): 14 | return 'No such command: ' in command.output 15 | 16 | 17 | def _get_operations(): 18 | proc = subprocess.Popen('yum', stdout=subprocess.PIPE) 19 | 20 | lines = proc.stdout.readlines() 21 | lines = [line.decode('utf-8') for line in lines] 22 | lines = dropwhile(lambda line: not line.startswith("List of Commands:"), lines) 23 | lines = islice(lines, 2, None) 24 | lines = list(takewhile(lambda line: line.strip(), lines)) 25 | return [line.strip().split(' ')[0] for line in lines] 26 | 27 | 28 | if which('yum'): 29 | _get_operations = cache(which('yum'))(_get_operations) 30 | 31 | 32 | @sudo_support 33 | def get_new_command(command): 34 | invalid_operation = command.script_parts[1] 35 | 36 | if invalid_operation == 'uninstall': 37 | return [command.script.replace('uninstall', 'remove')] 38 | 39 | return replace_command(command, invalid_operation, _get_operations()) 40 | -------------------------------------------------------------------------------- /tests/rules/test_git_stash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.git_stash import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | cherry_pick_error = ( 7 | 'error: Your local changes would be overwritten by cherry-pick.\n' 8 | 'hint: Commit your changes or stash them to proceed.\n' 9 | 'fatal: cherry-pick failed') 10 | 11 | 12 | rebase_error = ( 13 | 'Cannot rebase: Your index contains uncommitted changes.\n' 14 | 'Please commit or stash them.') 15 | 16 | 17 | @pytest.mark.parametrize('command', [ 18 | Command('git cherry-pick a1b2c3d', cherry_pick_error), 19 | Command('git rebase -i HEAD~7', rebase_error)]) 20 | def test_match(command): 21 | assert match(command) 22 | 23 | 24 | @pytest.mark.parametrize('command', [ 25 | Command('git cherry-pick a1b2c3d', ''), 26 | Command('git rebase -i HEAD~7', '')]) 27 | def test_not_match(command): 28 | assert not match(command) 29 | 30 | 31 | @pytest.mark.parametrize('command, new_command', [ 32 | (Command('git cherry-pick a1b2c3d', cherry_pick_error), 33 | 'git stash && git cherry-pick a1b2c3d'), 34 | (Command('git rebase -i HEAD~7', rebase_error), 35 | 'git stash && git rebase -i HEAD~7')]) 36 | def test_get_new_command(command, new_command): 37 | assert get_new_command(command) == new_command 38 | -------------------------------------------------------------------------------- /tests/rules/test_mkdir_p.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thefuck.rules.mkdir_p import match, get_new_command 3 | from thefuck.types import Command 4 | 5 | 6 | @pytest.mark.parametrize('command', [ 7 | Command('mkdir foo/bar/baz', 'mkdir: foo/bar: No such file or directory'), 8 | Command('./bin/hdfs dfs -mkdir foo/bar/baz', 'mkdir: `foo/bar/baz\': No such file or directory'), 9 | Command('hdfs dfs -mkdir foo/bar/baz', 'mkdir: `foo/bar/baz\': No such file or directory') 10 | ]) 11 | def test_match(command): 12 | assert match(command) 13 | 14 | 15 | @pytest.mark.parametrize('command', [ 16 | Command('mkdir foo/bar/baz', ''), 17 | Command('mkdir foo/bar/baz', 'foo bar baz'), 18 | Command('hdfs dfs -mkdir foo/bar/baz', ''), 19 | Command('./bin/hdfs dfs -mkdir foo/bar/baz', ''), 20 | Command('', ''), 21 | ]) 22 | def test_not_match(command): 23 | assert not match(command) 24 | 25 | 26 | @pytest.mark.parametrize('command, new_command', [ 27 | (Command('mkdir foo/bar/baz', ''), 'mkdir -p foo/bar/baz'), 28 | (Command('hdfs dfs -mkdir foo/bar/baz', ''), 'hdfs dfs -mkdir -p foo/bar/baz'), 29 | (Command('./bin/hdfs dfs -mkdir foo/bar/baz', ''), './bin/hdfs dfs -mkdir -p foo/bar/baz'), 30 | ]) 31 | def test_get_new_command(command, new_command): 32 | assert get_new_command(command) == new_command 33 | --------------------------------------------------------------------------------