diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..aac757a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,37 @@ + + + + +The output of `thefuck --version` (something like `The Fuck 3.1 using Python 3.5.0`): + + FILL THIS IN + +Your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.): + + FILL THIS IN + +Your system (Debian 7, ArchLinux, Windows, etc.): + + + +How to reproduce the bug: + + FILL THIS IN + +The output of The Fuck with `THEFUCK_DEBUG=true` exported (typically execute `export THEFUCK_DEBUG=true` in your shell before The Fuck): + + FILL THIS IN + +If the bug only appears with a specific application, the output of that application and its version: + + FILL THIS IN + +Anything else you think is relevant: + + + + diff --git a/.travis.yml b/.travis.yml index 990a7f0..3bc2c4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,6 @@ matrix: - os: linux dist: trusty python: "3.4" - - os: linux - dist: trusty - python: "3.3" - os: linux dist: trusty python: "2.7" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8e8d74..592a8ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,3 +23,37 @@ It's only with enough information that we can do something to fix the problem. We gladly accept pull request on the [official repository](https://github.com/nvbn/thefuck) for new rules, new features, bug fixes, etc. + +# Developing + +Install `The Fuck` for development: + +```bash +pip install -r requirements.txt +python setup.py develop +``` + +Run code style checks: + +```bash +flake8 +``` + +Run unit tests: + +```bash +py.test +``` + +Run unit and functional tests (requires docker): + +```bash +py.test --enable-functional +``` + +For sending package to pypi: + +```bash +sudo apt-get install pandoc +./release.py +``` diff --git a/LICENSE.md b/LICENSE.md index d79db85..ec1d1e2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License (MIT) ===================== -Copyright (c) 2015 Vladimir Iakovlev +Copyright (c) 2015-2018 Vladimir Iakovlev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 94fff46..8b9decf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Magnificent app which corrects your previous console command, inspired by a [@liamosaur](https://twitter.com/liamosaur/) [tweet](https://twitter.com/liamosaur/status/506975850596536320). -The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode) +The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode) [![gif with examples][examples-link]][examples-link] @@ -92,7 +92,7 @@ Reading package lists... Done ## Requirements -- python (3.3+) +- python (3.4+) - pip - python-dev @@ -111,6 +111,12 @@ sudo apt install python3-dev python3-pip sudo pip3 install thefuck ``` +On FreeBSD you can install `The Fuck` with: +```bash +sudo portsnap fetch update +cd /usr/ports/misc/thefuck && sudo make install clean +``` + On other systems you can install `The Fuck` with `pip`: ```bash @@ -148,7 +154,7 @@ fuck -r ## Update ```bash -pip install thefuck --upgrade +pip3 install thefuck --upgrade ``` **Aliases changed in 1.34.** @@ -158,6 +164,7 @@ pip install thefuck --upgrade The Fuck tries to match a rule for the previous command, creates a new command using the matched rule and runs it. Rules enabled by default are as follows: +* `adb_unknown_command` – fixes misspelled commands like `adb logcta`; * `ag_literal` – adds `-Q` to `ag` when suggested; * `aws_cli` – fixes misspelled commands like `aws dynamdb scan`; * `cargo` – runs `cargo build` instead of `cargo`; @@ -186,11 +193,14 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_branch_exists` – offers `git branch -d foo`, `git branch -D foo` or `git checkout foo` when creating a branch that already exists; * `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch; * `git_checkout` – fixes branch name or creates new branch; +* `git_commit_amend` – offers `git commit --amend` after previous commit; * `git_diff_no_index` – adds `--no-index` to previous `git diff` on untracked files; * `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output; * `git_fix_stash` – fixes `git stash` commands (misspelled subcommand and missing `save`); * `git_flag_after_filename` – fixes `fatal: bad flag '...' after filename` * `git_help_aliased` – fixes `git help ` commands replacing with the aliased command; +* `git_merge` – adds remote to branch names; +* `git_merge_unrelated` – adds `--allow-unrelated-histories` when required * `git_not_command` – fixes wrong git commands like `git brnch`; * `git_pull` – sets upstream before executing previous `git pull`; * `git_pull_clone` – clones instead of pulling when the repo does not exist; @@ -268,6 +278,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `tsuru_not_command` – fixes wrong `tsuru` commands like `tsuru shell`; * `tmux` – fixes `tmux` commands; * `unknown_command` – fixes hadoop hdfs-style "unknown command", for example adds missing '-' to the command on `hdfs dfs ls`; +* `unsudo` – removes `sudo` from previous command if a process refuses to run on super user privilege. * `vagrant_up` – starts up the vagrant instance; * `whois` – fixes `whois` command; * `workon_doesnt_exists` – fixes `virtualenvwrapper` env name os suggests to create new. @@ -282,6 +293,7 @@ Enabled by default only on specific platforms: * `apt_get_search` – changes trying to search using `apt-get` with searching using `apt-cache`; * `apt_invalid_operation` – fixes invalid `apt` and `apt-get` calls, like `apt-get isntall vim`; * `apt_list_upgradable` – helps you run `apt list --upgradable` after `apt update`; +* `apt_upgrade` – helps you run `apt upgrade` after `apt list --upgradable`; * `brew_cask_dependency` – installs cask dependencies; * `brew_install` – fixes formula name for `brew install`; * `brew_link` – adds `--overwrite --dry-run` if linking fails; @@ -443,37 +455,7 @@ eval $(thefuck --alias --enable-experimental-instant-mode) ## Developing -Install `The Fuck` for development: - -```bash -pip install -r requirements.txt -python setup.py develop -``` - -Run code style checks: - -```bash -flake8 -``` - -Run unit tests: - -```bash -py.test -``` - -Run unit and functional tests (requires docker): - -```bash -py.test --enable-functional -``` - -For sending package to pypi: - -```bash -sudo apt-get install pandoc -./release.py -``` +See [CONTRIBUTING.md](CONTRIBUTING.md) ## License MIT Project License can be found [here](LICENSE.md). diff --git a/appveyor.yml b/appveyor.yml index da1711d..541a69e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,6 @@ build: false environment: matrix: - PYTHON: "C:/Python27" - - PYTHON: "C:/Python33" - PYTHON: "C:/Python34" - PYTHON: "C:/Python35" - PYTHON: "C:/Python36" diff --git a/setup.py b/setup.py index 69d4aac..6ace6ef 100755 --- a/setup.py +++ b/setup.py @@ -26,8 +26,8 @@ if version < (2, 7): print('thefuck requires Python version 2.7 or later' + ' ({}.{} detected).'.format(*version)) sys.exit(-1) -elif (3, 0) < version < (3, 3): - print('thefuck requires Python version 3.3 or later' + +elif (3, 0) < version < (3, 4): + print('thefuck requires Python version 3.4 or later' + ' ({}.{} detected).'.format(*version)) sys.exit(-1) diff --git a/tests/rules/test_adb_unknown_command.py b/tests/rules/test_adb_unknown_command.py new file mode 100644 index 0000000..f2c3256 --- /dev/null +++ b/tests/rules/test_adb_unknown_command.py @@ -0,0 +1,41 @@ +import pytest +from thefuck.rules.adb_unknown_command import match, get_new_command +from thefuck.types import Command + + +@pytest.fixture +def output(): + return '''Android Debug Bridge version 1.0.31 + + -d - directs command to the only connected USB device + returns an error if more than one USB device is present. + -e - directs command to the only running emulator. + returns an error if more than one emulator is running. + -s - directs command to the device or emulator with the given + serial number or qualifier. Overrides ANDROID_SERIAL + environment variable. +''' + + +@pytest.mark.parametrize('script', [ + ('adb lgcat'), + ('adb puhs')]) +def test_match(output, script): + assert match(Command(script, output)) + + +@pytest.mark.parametrize('script', [ + 'git branch foo', + 'abd push']) +def test_not_match(script): + assert not match(Command(script, '')) + + +@pytest.mark.parametrize('script, new_command', [ + ('adb puhs test.bin /sdcard/test.bin', 'adb push test.bin /sdcard/test.bin'), + ('adb -s 1111 logcta', 'adb -s 1111 logcat'), + ('adb -P 666 pulll /sdcard/test.bin', 'adb -P 666 pull /sdcard/test.bin'), + ('adb -d logcatt', 'adb -d logcat'), + ('adb -e reboott', 'adb -e reboot')]) +def test_get_new_command(script, output, new_command): + assert get_new_command(Command(script, output)) == new_command diff --git a/tests/rules/test_apt_list_upgradable.py b/tests/rules/test_apt_list_upgradable.py index 567b855..257a92a 100644 --- a/tests/rules/test_apt_list_upgradable.py +++ b/tests/rules/test_apt_list_upgradable.py @@ -69,4 +69,7 @@ def test_not_match(command): def test_get_new_command(): new_command = get_new_command(Command('sudo apt update', match_output)) + assert new_command == 'sudo apt list --upgradable' + + new_command = get_new_command(Command('apt update', match_output)) assert new_command == 'apt list --upgradable' diff --git a/tests/rules/test_apt_upgrade.py b/tests/rules/test_apt_upgrade.py new file mode 100644 index 0000000..687d706 --- /dev/null +++ b/tests/rules/test_apt_upgrade.py @@ -0,0 +1,36 @@ +import pytest +from thefuck.rules.apt_upgrade import get_new_command, match +from thefuck.types import Command + +match_output = ''' +Listing... Done +heroku/stable 6.15.2-1 amd64 [upgradable from: 6.14.43-1] +resolvconf/zesty-updates,zesty-updates 1.79ubuntu4.1 all [upgradable from: 1.79ubuntu4] +squashfs-tools/zesty-updates 1:4.3-3ubuntu2.17.04.1 amd64 [upgradable from: 1:4.3-3ubuntu2] +unattended-upgrades/zesty-updates,zesty-updates 0.93.1ubuntu2.4 all [upgradable from: 0.93.1ubuntu2.3] +''' + +no_match_output = ''' +Listing... Done +''' + + +def test_match(): + assert match(Command('apt list --upgradable', match_output)) + assert match(Command('sudo apt list --upgradable', match_output)) + + +@pytest.mark.parametrize('command', [ + Command('apt list --upgradable', no_match_output), + Command('sudo apt list --upgradable', no_match_output) +]) +def test_not_match(command): + assert not match(command) + + +def test_get_new_command(): + new_command = get_new_command(Command('apt list --upgradable', match_output)) + assert new_command == 'apt upgrade' + + new_command = get_new_command(Command('sudo apt list --upgradable', match_output)) + assert new_command == 'sudo apt upgrade' diff --git a/tests/rules/test_git_branch_exists.py b/tests/rules/test_git_branch_exists.py index 14c17e5..b6e0df6 100644 --- a/tests/rules/test_git_branch_exists.py +++ b/tests/rules/test_git_branch_exists.py @@ -4,8 +4,8 @@ from thefuck.types import Command @pytest.fixture -def output(branch_name): - return "fatal: A branch named '{}' already exists.".format(branch_name) +def output(src_branch_name): + return "fatal: A branch named '{}' already exists.".format(src_branch_name) @pytest.fixture @@ -17,18 +17,25 @@ def new_command(branch_name): 'git branch -D {0} && git checkout -b {0}', 'git checkout {0}']] -@pytest.mark.parametrize('script, branch_name', [ - ('git branch foo', 'foo'), ('git checkout bar', 'bar')]) +@pytest.mark.parametrize('script, src_branch_name, branch_name', [ + ('git branch foo', 'foo', 'foo'), + ('git checkout bar', 'bar', 'bar'), + ('git checkout -b "let\'s-push-this"', '"let\'s-push-this"', '"let\'s-push-this"')]) def test_match(output, script, branch_name): assert match(Command(script, output)) -@pytest.mark.parametrize('script', ['git branch foo', 'git checkout bar']) +@pytest.mark.parametrize('script', [ + 'git branch foo', + 'git checkout bar', + 'git checkout -b "let\'s-push-this"']) def test_not_match(script): assert not match(Command(script, '')) -@pytest.mark.parametrize('script, branch_name, ', [ - ('git branch foo', 'foo'), ('git checkout bar', 'bar')]) -def test_get_new_command(output, new_command, script, branch_name): +@pytest.mark.parametrize('script, src_branch_name, branch_name', [ + ('git branch foo', 'foo', 'foo'), + ('git checkout bar', 'bar', 'bar'), + ('git checkout -b "let\'s-push-this"', "let's-push-this", "let\\'s-push-this")]) +def test_get_new_command(output, new_command, script, src_branch_name, branch_name): assert get_new_command(Command(script, output)) == new_command diff --git a/tests/rules/test_git_checkout.py b/tests/rules/test_git_checkout.py index 355b686..a803f20 100644 --- a/tests/rules/test_git_checkout.py +++ b/tests/rules/test_git_checkout.py @@ -52,7 +52,7 @@ def test_get_branches(branches, branch_list, git_branch): @pytest.mark.parametrize('branches, command, new_command', [ (b'', Command('git checkout unknown', did_not_match('unknown')), - 'git branch unknown && git checkout unknown'), + 'git checkout -b unknown'), (b'', Command('git commit unknown', did_not_match('unknown')), 'git branch unknown && git commit unknown'), diff --git a/tests/rules/test_git_commit_amend.py b/tests/rules/test_git_commit_amend.py new file mode 100644 index 0000000..743ccbf --- /dev/null +++ b/tests/rules/test_git_commit_amend.py @@ -0,0 +1,25 @@ +import pytest +from thefuck.rules.git_commit_amend import match, get_new_command +from thefuck.types import Command + + +@pytest.mark.parametrize('script, output', [ + ('git commit -m "test"', 'test output'), + ('git commit', '')]) +def test_match(output, script): + assert match(Command(script, output)) + + +@pytest.mark.parametrize('script', [ + 'git branch foo', + 'git checkout feature/test_commit', + 'git push']) +def test_not_match(script): + assert not match(Command(script, '')) + + +@pytest.mark.parametrize('script', [ + ('git commit -m "test commit"'), + ('git commit')]) +def test_get_new_command(script): + assert get_new_command(Command(script, '')) == 'git commit --amend' diff --git a/tests/rules/test_git_merge.py b/tests/rules/test_git_merge.py new file mode 100644 index 0000000..c347a59 --- /dev/null +++ b/tests/rules/test_git_merge.py @@ -0,0 +1,26 @@ +import pytest +from thefuck.rules.git_merge import match, get_new_command +from thefuck.types import Command + + +@pytest.fixture +def output(): + return 'merge: local - not something we can merge\n\n' \ + 'Did you mean this?\n\tremote/local' + + +def test_match(output): + assert match(Command('git merge test', output)) + assert not match(Command('git merge master', '')) + assert not match(Command('ls', output)) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('git merge local', output()), + 'git merge remote/local'), + (Command('git merge -m "test" local', output()), + 'git merge -m "test" remote/local'), + (Command('git merge -m "test local" local', output()), + 'git merge -m "test local" remote/local')]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/tests/rules/test_git_merge_unrelated.py b/tests/rules/test_git_merge_unrelated.py new file mode 100644 index 0000000..c9a2b9e --- /dev/null +++ b/tests/rules/test_git_merge_unrelated.py @@ -0,0 +1,25 @@ +import pytest +from thefuck.rules.git_merge_unrelated import match, get_new_command +from thefuck.types import Command + + +@pytest.fixture +def output(): + return 'fatal: refusing to merge unrelated histories' + + +def test_match(output): + assert match(Command('git merge test', output)) + assert not match(Command('git merge master', '')) + assert not match(Command('ls', output)) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('git merge local', output()), + 'git merge local --allow-unrelated-histories'), + (Command('git merge -m "test" local', output()), + 'git merge -m "test" local --allow-unrelated-histories'), + (Command('git merge -m "test local" local', output()), + 'git merge -m "test local" local --allow-unrelated-histories')]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/tests/rules/test_git_push.py b/tests/rules/test_git_push.py index debd703..b6aa7c6 100644 --- a/tests/rules/test_git_push.py +++ b/tests/rules/test_git_push.py @@ -4,30 +4,68 @@ from thefuck.types import Command @pytest.fixture -def output(): - return '''fatal: The current branch master has no upstream branch. +def output(branch_name): + if not branch_name: + return '' + return '''fatal: The current branch {} has no upstream branch. To push the current branch and set the remote as upstream, use - git push --set-upstream origin master + git push --set-upstream origin {} +'''.format(branch_name, branch_name) + + +@pytest.fixture +def output_bitbucket(): + return '''Total 0 (delta 0), reused 0 (delta 0) +remote: +remote: Create pull request for feature/set-upstream: +remote: https://bitbucket.org/set-upstream +remote: +To git@bitbucket.org:test.git + e5e7fbb..700d998 feature/set-upstream -> feature/set-upstream +Branch feature/set-upstream set up to track remote branch feature/set-upstream from origin. ''' -def test_match(output): - assert match(Command('git push', output)) - assert match(Command('git push master', output)) - assert not match(Command('git push master', '')) - assert not match(Command('ls', output)) +@pytest.mark.parametrize('script, branch_name', [ + ('git push', 'master'), + ('git push origin', 'master')]) +def test_match(output, script, branch_name): + assert match(Command(script, output)) -def test_get_new_command(output): - assert get_new_command(Command('git push', output))\ - == "git push --set-upstream origin master" - assert get_new_command(Command('git push -u', output))\ - == "git push --set-upstream origin master" - assert get_new_command(Command('git push -u origin', output))\ - == "git push --set-upstream origin master" - assert get_new_command(Command('git push --set-upstream origin', output))\ - == "git push --set-upstream origin master" - assert get_new_command(Command('git push --quiet', output))\ - == "git push --set-upstream origin master --quiet" +def test_match_bitbucket(output_bitbucket): + assert not match(Command('git push origin', output_bitbucket)) + + +@pytest.mark.parametrize('script, branch_name', [ + ('git push master', None), + ('ls', 'master')]) +def test_not_match(output, script, branch_name): + assert not match(Command(script, output)) + + +@pytest.mark.parametrize('script, branch_name, new_command', [ + ('git push', 'master', + 'git push --set-upstream origin master'), + ('git push master', 'master', + 'git push --set-upstream origin master'), + ('git push -u', 'master', + 'git push --set-upstream origin master'), + ('git push -u origin', 'master', + 'git push --set-upstream origin master'), + ('git push origin', 'master', + 'git push --set-upstream origin master'), + ('git push --set-upstream origin', 'master', + 'git push --set-upstream origin master'), + ('git push --quiet', 'master', + 'git push --set-upstream origin master --quiet'), + ('git push --quiet origin', 'master', + 'git push --set-upstream origin master --quiet'), + ('git -c test=test push --quiet origin', 'master', + 'git -c test=test push --set-upstream origin master --quiet'), + ('git push', "test's", + "git push --set-upstream origin test\\'s")]) +def test_get_new_command(output, script, branch_name, new_command): + assert get_new_command(Command(script, output)) == new_command diff --git a/tests/rules/test_switch_lang.py b/tests/rules/test_switch_lang.py index 5d22f94..e2da837 100644 --- a/tests/rules/test_switch_lang.py +++ b/tests/rules/test_switch_lang.py @@ -7,7 +7,9 @@ from thefuck.types import Command @pytest.mark.parametrize('command', [ Command(u'фзе-пуе', 'command not found: фзе-пуе'), - Command(u'λσ', 'command not found: λσ')]) + Command(u'λσ', 'command not found: λσ'), + Command(u'שפא-עקא', 'command not found: שפא-עקא'), + Command(u'ךד', 'command not found: ךד')]) def test_match(command): assert switch_lang.match(command) @@ -16,13 +18,16 @@ def test_match(command): Command(u'pat-get', 'command not found: pat-get'), Command(u'ls', 'command not found: ls'), Command(u'агсл', 'command not found: агсл'), - Command(u'фзе-пуе', 'some info')]) + Command(u'фзе-пуе', 'some info'), + Command(u'שפא-עקא', 'some info')]) def test_not_match(command): assert not switch_lang.match(command) @pytest.mark.parametrize('command, new_command', [ (Command(u'фзе-пуе штыефдд мшь', ''), 'apt-get install vim'), - (Command(u'λσ -λα', ''), 'ls -la')]) + (Command(u'λσ -λα', ''), 'ls -la'), + (Command(u'שפא-עקא ןמדאשךך הןצ', ''), 'apt-get install vim'), + (Command(u'ךד -ךש', ''), 'ls -la')]) def test_get_new_command(command, new_command): assert switch_lang.get_new_command(command) == new_command diff --git a/tests/rules/test_unsudo.py b/tests/rules/test_unsudo.py new file mode 100644 index 0000000..7f22599 --- /dev/null +++ b/tests/rules/test_unsudo.py @@ -0,0 +1,22 @@ +import pytest +from thefuck.rules.unsudo import match, get_new_command +from thefuck.types import Command + + +@pytest.mark.parametrize('output', [ + 'you cannot perform this operation as root']) +def test_match(output): + assert match(Command('sudo ls', output)) + + +def test_not_match(): + assert not match(Command('', '')) + assert not match(Command('sudo ls', 'Permission denied')) + assert not match(Command('ls', 'you cannot perform this operation as root')) + + +@pytest.mark.parametrize('before, after', [ + ('sudo ls', 'ls'), + ('sudo pacaur -S helloworld', 'pacaur -S helloworld')]) +def test_get_new_command(before, after): + assert get_new_command(Command(before, '')) == after diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index 117e3af..b10ae0e 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -51,6 +51,7 @@ class TestBash(object): def test_app_alias_variables_correctly_set(self, shell): alias = shell.app_alias('fuck') assert "fuck () {" in alias + assert 'TF_SHELL=bash' in alias assert "TF_ALIAS=fuck" in alias assert 'PYTHONIOENCODING=utf-8' in alias assert 'TF_SHELL_ALIASES=$(alias)' in alias diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py index 836a648..efd684f 100644 --- a/tests/shells/test_fish.py +++ b/tests/shells/test_fish.py @@ -13,9 +13,10 @@ class TestFish(object): @pytest.fixture(autouse=True) def Popen(self, mocker): mock = mocker.patch('thefuck.shells.fish.Popen') - mock.return_value.stdout.read.return_value = ( + mock.return_value.stdout.read.side_effect = [( b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n' - b'man\nmath\npopd\npushd\nruby') + b'man\nmath\npopd\npushd\nruby'), + b'alias fish_key_reader /usr/bin/fish_key_reader\nalias g git'] return mock @pytest.mark.parametrize('key, value', [ @@ -42,7 +43,8 @@ class TestFish(object): ('open', 'open'), ('vim', 'vim'), ('ll', 'fish -ic "ll"'), - ('ls', 'ls')]) # Fish has no aliases but functions + ('ls', 'ls'), + ('g', 'git')]) def test_from_shell(self, before, after, shell): assert shell.from_shell(before) == after @@ -65,12 +67,15 @@ class TestFish(object): 'math': 'math', 'popd': 'popd', 'pushd': 'pushd', - 'ruby': 'ruby'} + 'ruby': 'ruby', + 'g': 'git', + 'fish_key_reader': '/usr/bin/fish_key_reader'} def test_app_alias(self, shell): assert 'function fuck' in shell.app_alias('fuck') assert 'function FUCK' in shell.app_alias('FUCK') assert 'thefuck' in shell.app_alias('fuck') + assert 'TF_SHELL=fish' in shell.app_alias('fuck') assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') diff --git a/tests/shells/test_tcsh.py b/tests/shells/test_tcsh.py index 7bc8ae3..014c3c9 100644 --- a/tests/shells/test_tcsh.py +++ b/tests/shells/test_tcsh.py @@ -44,6 +44,7 @@ class TestTcsh(object): 'll': 'ls -alF'} def test_app_alias(self, shell): + assert 'setenv TF_SHELL tcsh' in shell.app_alias('fuck') assert 'alias fuck' in shell.app_alias('fuck') assert 'alias FUCK' in shell.app_alias('FUCK') assert 'thefuck' in shell.app_alias('fuck') diff --git a/tests/shells/test_zsh.py b/tests/shells/test_zsh.py index e31c362..fc20a06 100644 --- a/tests/shells/test_zsh.py +++ b/tests/shells/test_zsh.py @@ -51,6 +51,7 @@ class TestZsh(object): def test_app_alias_variables_correctly_set(self, shell): alias = shell.app_alias('fuck') assert "fuck () {" in alias + assert 'TF_SHELL=zsh' in alias assert "TF_ALIAS=fuck" in alias assert 'PYTHONIOENCODING=utf-8' in alias assert 'TF_SHELL_ALIASES=$(alias)' in alias diff --git a/thefuck/rules/adb_unknown_command.py b/thefuck/rules/adb_unknown_command.py new file mode 100644 index 0000000..bd33ca1 --- /dev/null +++ b/thefuck/rules/adb_unknown_command.py @@ -0,0 +1,54 @@ +from thefuck.utils import is_app, get_closest, replace_argument + + +_ADB_COMMANDS = ( + 'backup', + 'bugreport', + 'connect', + 'devices', + 'disable-verity', + 'disconnect', + 'enable-verity', + 'emu', + 'forward', + 'get-devpath', + 'get-serialno', + 'get-state', + 'install', + 'install-multiple', + 'jdwp', + 'keygen', + 'kill-server', + 'logcat', + 'pull', + 'push', + 'reboot', + 'reconnect', + 'restore', + 'reverse', + 'root', + 'run-as', + 'shell', + 'sideload', + 'start-server', + 'sync', + 'tcpip', + 'uninstall', + 'unroot', + 'usb', + 'wait-for', +) + + +def match(command): + return (is_app(command, 'adb') + and command.output.startswith('Android Debug Bridge version')) + + +def get_new_command(command): + for idx, arg in enumerate(command.script_parts[1:]): + # allowed params to ADB are a/d/e/s/H/P/L where s, H, P and L take additional args + # for example 'adb -s 111 logcat' or 'adb -e logcat' + if not arg[0] == '-' and not command.script_parts[idx] in ('-s', '-H', '-P', '-L'): + adb_cmd = get_closest(arg, _ADB_COMMANDS) + return replace_argument(command.script, arg, adb_cmd) diff --git a/thefuck/rules/apt_list_upgradable.py b/thefuck/rules/apt_list_upgradable.py index 767b201..071a748 100644 --- a/thefuck/rules/apt_list_upgradable.py +++ b/thefuck/rules/apt_list_upgradable.py @@ -11,5 +11,6 @@ def match(command): return "Run 'apt list --upgradable' to see them." in command.output +@sudo_support def get_new_command(command): return 'apt list --upgradable' diff --git a/thefuck/rules/apt_upgrade.py b/thefuck/rules/apt_upgrade.py new file mode 100644 index 0000000..6beb719 --- /dev/null +++ b/thefuck/rules/apt_upgrade.py @@ -0,0 +1,16 @@ +from thefuck.specific.apt import apt_available +from thefuck.specific.sudo import sudo_support +from thefuck.utils import for_app + +enabled_by_default = apt_available + + +@sudo_support +@for_app('apt') +def match(command): + return command.script == "apt list --upgradable" and len(command.output.strip().split('\n')) > 1 + + +@sudo_support +def get_new_command(command): + return 'apt upgrade' diff --git a/thefuck/rules/git_branch_exists.py b/thefuck/rules/git_branch_exists.py index 93e11b5..4a7a822 100644 --- a/thefuck/rules/git_branch_exists.py +++ b/thefuck/rules/git_branch_exists.py @@ -7,14 +7,15 @@ from thefuck.utils import eager @git_support def match(command): return ("fatal: A branch named '" in command.output - and " already exists." in command.output) + and "' already exists." in command.output) @git_support @eager def get_new_command(command): branch_name = re.findall( - r"fatal: A branch named '([^']*)' already exists.", command.output)[0] + r"fatal: A branch named '(.+)' already exists.", command.output)[0] + branch_name = branch_name.replace("'", r"\'") new_command_templates = [['git branch -d {0}', 'git branch {0}'], ['git branch -d {0}', 'git checkout -b {0}'], ['git branch -D {0}', 'git branch {0}'], diff --git a/thefuck/rules/git_checkout.py b/thefuck/rules/git_checkout.py index 934f0a4..65705f4 100644 --- a/thefuck/rules/git_checkout.py +++ b/thefuck/rules/git_checkout.py @@ -34,6 +34,8 @@ def get_new_command(command): fallback_to_first=False) if closest_branch: return replace_argument(command.script, missing_file, closest_branch) + elif command.script_parts[1] == 'checkout': + return replace_argument(command.script, 'checkout', 'checkout -b') else: return shell.and_('git branch {}', '{}').format( missing_file, command.script) diff --git a/thefuck/rules/git_commit_amend.py b/thefuck/rules/git_commit_amend.py new file mode 100644 index 0000000..dd0ae6d --- /dev/null +++ b/thefuck/rules/git_commit_amend.py @@ -0,0 +1,11 @@ +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('commit' in command.script_parts) + + +@git_support +def get_new_command(command): + return 'git commit --amend' diff --git a/thefuck/rules/git_merge.py b/thefuck/rules/git_merge.py new file mode 100644 index 0000000..2420b67 --- /dev/null +++ b/thefuck/rules/git_merge.py @@ -0,0 +1,18 @@ +import re +from thefuck.utils import replace_argument +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('merge' in command.script + and ' - not something we can merge' in command.output + and 'Did you mean this?' in command.output) + + +@git_support +def get_new_command(command): + unknown_branch = re.findall(r'merge: (.+) - not something we can merge', command.output)[0] + remote_branch = re.findall(r'Did you mean this\?\n\t([^\n]+)', command.output)[0] + + return replace_argument(command.script, unknown_branch, remote_branch) diff --git a/thefuck/rules/git_merge_unrelated.py b/thefuck/rules/git_merge_unrelated.py new file mode 100644 index 0000000..8e32642 --- /dev/null +++ b/thefuck/rules/git_merge_unrelated.py @@ -0,0 +1,12 @@ +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('merge' in command.script + and 'fatal: refusing to merge unrelated histories' in command.output) + + +@git_support +def get_new_command(command): + return command.script + ' --allow-unrelated-histories' diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index 7a47240..cccee67 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -5,8 +5,8 @@ from thefuck.specific.git import git_support @git_support def match(command): - return ('push' in command.script - and 'set-upstream' in command.output) + return ('push' in command.script_parts + and 'git push --set-upstream' in command.output) def _get_upstream_option_index(command_parts): @@ -32,7 +32,13 @@ def get_new_command(command): # In case of `git push -u` we don't have next argument: if len(command_parts) > upstream_option_index: command_parts.pop(upstream_option_index) + else: + # the only non-qualified permitted options are the repository and refspec; git's + # suggestion include them, so they won't be lost, but would be duplicated otherwise. + push_idx = command_parts.index('push') + 1 + while len(command_parts) > push_idx and command_parts[len(command_parts) - 1][0] != '-': + command_parts.pop(len(command_parts) - 1) - arguments = re.findall(r'git push (.*)', command.output)[0].strip() + arguments = re.findall(r'git push (.*)', command.output)[0].replace("'", r"\'").strip() return replace_argument(" ".join(command_parts), 'push', 'push {}'.format(arguments)) diff --git a/thefuck/rules/switch_lang.py b/thefuck/rules/switch_lang.py index e67c89d..49ceec0 100644 --- a/thefuck/rules/switch_lang.py +++ b/thefuck/rules/switch_lang.py @@ -5,7 +5,8 @@ target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVB source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''', u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''', - u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?'''] + u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?''', + u'''/'קראטוןםפ][שדגכעיחלךף,זסבהנמצתץ.QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''] @memoize diff --git a/thefuck/rules/unsudo.py b/thefuck/rules/unsudo.py new file mode 100644 index 0000000..beeac7a --- /dev/null +++ b/thefuck/rules/unsudo.py @@ -0,0 +1,15 @@ +patterns = ['you cannot perform this operation as root'] + + +def match(command): + if command.script_parts and command.script_parts[0] != 'sudo': + return False + + for pattern in patterns: + if pattern in command.output.lower(): + return True + return False + + +def get_new_command(command): + return ' '.join(command.script_parts[1:]) diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 56ad214..471df2a 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -10,12 +10,24 @@ from .generic import Generic @cache('~/.config/fish/config.fish', '~/.config/fish/functions') -def _get_aliases(overridden): +def _get_functions(overridden): proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL) functions = proc.stdout.read().decode('utf-8').strip().split('\n') return {func: func for func in functions if func not in overridden} +@cache('~/.config/fish/config.fish') +def _get_aliases(overridden): + aliases = {} + proc = Popen(['fish', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) + alias_out = proc.stdout.read().decode('utf-8').strip().split('\n') + for alias in alias_out: + name, value = alias.replace('alias ', '', 1).split(' ', 1) + if name not in overridden: + aliases[name] = value + return aliases + + class Fish(Generic): def _get_overridden_aliases(self): overridden = os.environ.get('THEFUCK_OVERRIDDEN_ALIASES', @@ -35,7 +47,7 @@ class Fish(Generic): # It is VERY important to have the variables declared WITHIN the alias return ('function {0} -d "Correct your previous console command"\n' ' set -l fucked_up_command $history[1]\n' - ' env TF_ALIAS={0} PYTHONIOENCODING=utf-8' + ' env TF_SHELL=fish TF_ALIAS={0} PYTHONIOENCODING=utf-8' ' thefuck $fucked_up_command | read -l unfucked_command\n' ' if [ "$unfucked_command" != "" ]\n' ' eval $unfucked_command\n{1}' @@ -44,12 +56,17 @@ class Fish(Generic): def get_aliases(self): overridden = self._get_overridden_aliases() - return _get_aliases(overridden) + functions = _get_functions(overridden) + raw_aliases = _get_aliases(overridden) + functions.update(raw_aliases) + return functions def _expand_aliases(self, command_script): aliases = self.get_aliases() binary = command_script.split(' ')[0] - if binary in aliases: + if binary in aliases and aliases[binary] != binary: + return command_script.replace(binary, aliases[binary], 1) + elif binary in aliases: return u'fish -ic "{}"'.format(command_script.replace('"', r'\"')) else: return command_script diff --git a/thefuck/shells/tcsh.py b/thefuck/shells/tcsh.py index d8470eb..0911f04 100644 --- a/thefuck/shells/tcsh.py +++ b/thefuck/shells/tcsh.py @@ -7,7 +7,7 @@ from .generic import Generic class Tcsh(Generic): def app_alias(self, alias_name): - return ("alias {0} 'setenv TF_ALIAS {0} && " + return ("alias {0} 'setenv TF_SHELL tcsh && setenv TF_ALIAS {0} && " "set fucked_cmd=`history -h 2 | head -n 1` && " "eval `thefuck ${{fucked_cmd}}`'").format(alias_name) diff --git a/tox.ini b/tox.ini index 8fc3820..f299475 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35,py36 +envlist = py27,py34,py35,py36 [testenv] deps = -rrequirements.txt