thefuck-leeguoo 3.41__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thefuck/__init__.py +0 -0
- thefuck/ai.py +765 -0
- thefuck/argument_parser.py +96 -0
- thefuck/conf.py +141 -0
- thefuck/const.py +111 -0
- thefuck/corrector.py +92 -0
- thefuck/entrypoints/__init__.py +0 -0
- thefuck/entrypoints/alias.py +28 -0
- thefuck/entrypoints/fix_command.py +105 -0
- thefuck/entrypoints/main.py +50 -0
- thefuck/entrypoints/not_configured.py +201 -0
- thefuck/entrypoints/setup.py +227 -0
- thefuck/entrypoints/shell_logger.py +79 -0
- thefuck/exceptions.py +10 -0
- thefuck/logs.py +255 -0
- thefuck/output_readers/__init__.py +20 -0
- thefuck/output_readers/read_log.py +108 -0
- thefuck/output_readers/rerun.py +72 -0
- thefuck/output_readers/shell_logger.py +60 -0
- thefuck/rules/__init__.py +0 -0
- thefuck/rules/adb_unknown_command.py +54 -0
- thefuck/rules/ag_literal.py +10 -0
- thefuck/rules/apt_get.py +50 -0
- thefuck/rules/apt_get_search.py +14 -0
- thefuck/rules/apt_invalid_operation.py +63 -0
- thefuck/rules/apt_list_upgradable.py +16 -0
- thefuck/rules/apt_upgrade.py +16 -0
- thefuck/rules/aws_cli.py +17 -0
- thefuck/rules/az_cli.py +17 -0
- thefuck/rules/brew_cask_dependency.py +33 -0
- thefuck/rules/brew_install.py +24 -0
- thefuck/rules/brew_link.py +15 -0
- thefuck/rules/brew_reinstall.py +19 -0
- thefuck/rules/brew_uninstall.py +14 -0
- thefuck/rules/brew_unknown_command.py +82 -0
- thefuck/rules/brew_update_formula.py +12 -0
- thefuck/rules/cargo.py +6 -0
- thefuck/rules/cargo_no_command.py +15 -0
- thefuck/rules/cat_dir.py +14 -0
- thefuck/rules/cd_correction.py +61 -0
- thefuck/rules/cd_cs.py +21 -0
- thefuck/rules/cd_mkdir.py +21 -0
- thefuck/rules/cd_parent.py +16 -0
- thefuck/rules/chmod_x.py +15 -0
- thefuck/rules/choco_install.py +25 -0
- thefuck/rules/composer_not_command.py +22 -0
- thefuck/rules/conda_mistype.py +17 -0
- thefuck/rules/cp_create_destination.py +15 -0
- thefuck/rules/cp_omitting_directory.py +15 -0
- thefuck/rules/cpp11.py +12 -0
- thefuck/rules/dirty_untar.py +53 -0
- thefuck/rules/dirty_unzip.py +60 -0
- thefuck/rules/django_south_ghost.py +8 -0
- thefuck/rules/django_south_merge.py +8 -0
- thefuck/rules/dnf_no_such_command.py +37 -0
- thefuck/rules/docker_image_being_used_by_container.py +20 -0
- thefuck/rules/docker_login.py +13 -0
- thefuck/rules/docker_not_command.py +49 -0
- thefuck/rules/dry.py +15 -0
- thefuck/rules/fab_command_not_found.py +38 -0
- thefuck/rules/fix_alt_space.py +15 -0
- thefuck/rules/fix_file.py +80 -0
- thefuck/rules/gem_unknown_command.py +36 -0
- thefuck/rules/git_add.py +27 -0
- thefuck/rules/git_add_force.py +13 -0
- thefuck/rules/git_bisect_usage.py +16 -0
- thefuck/rules/git_branch_0flag.py +24 -0
- thefuck/rules/git_branch_delete.py +13 -0
- thefuck/rules/git_branch_delete_checked_out.py +19 -0
- thefuck/rules/git_branch_exists.py +25 -0
- thefuck/rules/git_branch_list.py +14 -0
- thefuck/rules/git_checkout.py +49 -0
- thefuck/rules/git_clone_git_clone.py +12 -0
- thefuck/rules/git_clone_missing.py +42 -0
- thefuck/rules/git_commit_add.py +17 -0
- thefuck/rules/git_commit_amend.py +11 -0
- thefuck/rules/git_commit_reset.py +11 -0
- thefuck/rules/git_diff_no_index.py +16 -0
- thefuck/rules/git_diff_staged.py +13 -0
- thefuck/rules/git_fix_stash.py +37 -0
- thefuck/rules/git_flag_after_filename.py +31 -0
- thefuck/rules/git_help_aliased.py +12 -0
- thefuck/rules/git_hook_bypass.py +27 -0
- thefuck/rules/git_lfs_mistype.py +18 -0
- thefuck/rules/git_main_master.py +16 -0
- thefuck/rules/git_merge.py +18 -0
- thefuck/rules/git_merge_unrelated.py +12 -0
- thefuck/rules/git_not_command.py +18 -0
- thefuck/rules/git_pull.py +16 -0
- thefuck/rules/git_pull_clone.py +13 -0
- thefuck/rules/git_pull_uncommitted_changes.py +14 -0
- thefuck/rules/git_push.py +44 -0
- thefuck/rules/git_push_different_branch_names.py +12 -0
- thefuck/rules/git_push_force.py +18 -0
- thefuck/rules/git_push_pull.py +20 -0
- thefuck/rules/git_push_without_commits.py +12 -0
- thefuck/rules/git_rebase_merge_dir.py +17 -0
- thefuck/rules/git_rebase_no_changes.py +13 -0
- thefuck/rules/git_remote_delete.py +13 -0
- thefuck/rules/git_remote_seturl_add.py +12 -0
- thefuck/rules/git_rm_local_modifications.py +19 -0
- thefuck/rules/git_rm_recursive.py +16 -0
- thefuck/rules/git_rm_staged.py +19 -0
- thefuck/rules/git_stash.py +15 -0
- thefuck/rules/git_stash_pop.py +18 -0
- thefuck/rules/git_tag_force.py +13 -0
- thefuck/rules/git_two_dashes.py +14 -0
- thefuck/rules/go_run.py +16 -0
- thefuck/rules/go_unknown_command.py +28 -0
- thefuck/rules/gradle_no_task.py +34 -0
- thefuck/rules/gradle_wrapper.py +13 -0
- thefuck/rules/grep_arguments_order.py +23 -0
- thefuck/rules/grep_recursive.py +10 -0
- thefuck/rules/grunt_task_not_found.py +37 -0
- thefuck/rules/gulp_not_task.py +22 -0
- thefuck/rules/has_exists_script.py +13 -0
- thefuck/rules/heroku_multiple_apps.py +12 -0
- thefuck/rules/heroku_not_command.py +11 -0
- thefuck/rules/history.py +15 -0
- thefuck/rules/hostscli.py +27 -0
- thefuck/rules/ifconfig_device_not_found.py +23 -0
- thefuck/rules/java.py +17 -0
- thefuck/rules/javac.py +18 -0
- thefuck/rules/lein_not_task.py +19 -0
- thefuck/rules/ln_no_hard_link.py +23 -0
- thefuck/rules/ln_s_order.py +26 -0
- thefuck/rules/long_form_help.py +27 -0
- thefuck/rules/ls_all.py +10 -0
- thefuck/rules/ls_lah.py +12 -0
- thefuck/rules/man.py +33 -0
- thefuck/rules/man_no_space.py +10 -0
- thefuck/rules/mercurial.py +27 -0
- thefuck/rules/missing_space_before_subcommand.py +21 -0
- thefuck/rules/mkdir_p.py +13 -0
- thefuck/rules/mvn_no_command.py +11 -0
- thefuck/rules/mvn_unknown_lifecycle_phase.py +30 -0
- thefuck/rules/nixos_cmd_not_found.py +15 -0
- thefuck/rules/no_command.py +41 -0
- thefuck/rules/no_such_file.py +30 -0
- thefuck/rules/npm_missing_script.py +17 -0
- thefuck/rules/npm_run_script.py +17 -0
- thefuck/rules/npm_wrong_command.py +42 -0
- thefuck/rules/omnienv_no_such_command.py +35 -0
- thefuck/rules/open.py +40 -0
- thefuck/rules/pacman.py +17 -0
- thefuck/rules/pacman_invalid_option.py +20 -0
- thefuck/rules/pacman_not_found.py +26 -0
- thefuck/rules/path_from_history.py +53 -0
- thefuck/rules/php_s.py +11 -0
- thefuck/rules/pip_install.py +15 -0
- thefuck/rules/pip_unknown_command.py +19 -0
- thefuck/rules/port_already_in_use.py +40 -0
- thefuck/rules/prove_recursively.py +27 -0
- thefuck/rules/python_command.py +17 -0
- thefuck/rules/python_execute.py +15 -0
- thefuck/rules/python_module_error.py +13 -0
- thefuck/rules/quotation_marks.py +12 -0
- thefuck/rules/rails_migrations_pending.py +14 -0
- thefuck/rules/react_native_command_unrecognized.py +34 -0
- thefuck/rules/remove_shell_prompt_literal.py +23 -0
- thefuck/rules/remove_trailing_cedilla.py +11 -0
- thefuck/rules/rm_dir.py +16 -0
- thefuck/rules/rm_root.py +16 -0
- thefuck/rules/scm_correction.py +32 -0
- thefuck/rules/sed_unterminated_s.py +18 -0
- thefuck/rules/sl_ls.py +14 -0
- thefuck/rules/ssh_known_hosts.py +37 -0
- thefuck/rules/sudo.py +47 -0
- thefuck/rules/sudo_command_from_user_path.py +21 -0
- thefuck/rules/switch_lang.py +117 -0
- thefuck/rules/systemctl.py +22 -0
- thefuck/rules/terraform_init.py +13 -0
- thefuck/rules/terraform_no_command.py +16 -0
- thefuck/rules/test.py.py +10 -0
- thefuck/rules/tmux.py +18 -0
- thefuck/rules/touch.py +14 -0
- thefuck/rules/tsuru_login.py +12 -0
- thefuck/rules/tsuru_not_command.py +15 -0
- thefuck/rules/unknown_command.py +13 -0
- thefuck/rules/unsudo.py +15 -0
- thefuck/rules/vagrant_up.py +21 -0
- thefuck/rules/whois.py +34 -0
- thefuck/rules/workon_doesnt_exists.py +32 -0
- thefuck/rules/wrong_hyphen_before_subcommand.py +20 -0
- thefuck/rules/yarn_alias.py +14 -0
- thefuck/rules/yarn_command_not_found.py +43 -0
- thefuck/rules/yarn_command_replaced.py +13 -0
- thefuck/rules/yarn_help.py +17 -0
- thefuck/rules/yum_invalid_operation.py +39 -0
- thefuck/shells/__init__.py +52 -0
- thefuck/shells/bash.py +94 -0
- thefuck/shells/fish.py +131 -0
- thefuck/shells/generic.py +154 -0
- thefuck/shells/powershell.py +43 -0
- thefuck/shells/tcsh.py +44 -0
- thefuck/shells/zsh.py +98 -0
- thefuck/specific/__init__.py +0 -0
- thefuck/specific/apt.py +3 -0
- thefuck/specific/archlinux.py +48 -0
- thefuck/specific/brew.py +15 -0
- thefuck/specific/dnf.py +3 -0
- thefuck/specific/git.py +32 -0
- thefuck/specific/nix.py +3 -0
- thefuck/specific/npm.py +21 -0
- thefuck/specific/sudo.py +18 -0
- thefuck/specific/yum.py +3 -0
- thefuck/system/__init__.py +7 -0
- thefuck/system/unix.py +57 -0
- thefuck/system/win32.py +43 -0
- thefuck/types.py +261 -0
- thefuck/ui.py +116 -0
- thefuck/utils.py +385 -0
- thefuck_leeguoo-3.41.dist-info/METADATA +681 -0
- thefuck_leeguoo-3.41.dist-info/RECORD +218 -0
- thefuck_leeguoo-3.41.dist-info/WHEEL +6 -0
- thefuck_leeguoo-3.41.dist-info/entry_points.txt +3 -0
- thefuck_leeguoo-3.41.dist-info/licenses/LICENSE.md +22 -0
- thefuck_leeguoo-3.41.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Initialize output before importing any module, that can use colorama.
|
|
2
|
+
from ..system import init_output
|
|
3
|
+
|
|
4
|
+
init_output()
|
|
5
|
+
|
|
6
|
+
import getpass # noqa: E402
|
|
7
|
+
import os # noqa: E402
|
|
8
|
+
import json # noqa: E402
|
|
9
|
+
import sys # noqa: E402
|
|
10
|
+
import subprocess # noqa: E402
|
|
11
|
+
from tempfile import gettempdir # noqa: E402
|
|
12
|
+
import time # noqa: E402
|
|
13
|
+
import six # noqa: E402
|
|
14
|
+
from psutil import Process # noqa: E402
|
|
15
|
+
from .. import logs, const, types # noqa: E402
|
|
16
|
+
from ..shells import shell # noqa: E402
|
|
17
|
+
from ..conf import settings # noqa: E402
|
|
18
|
+
from ..system import Path # noqa: E402
|
|
19
|
+
from ..corrector import get_corrected_commands # noqa: E402
|
|
20
|
+
from ..exceptions import EmptyCommand # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_shell_pid():
|
|
24
|
+
"""Returns parent process pid."""
|
|
25
|
+
proc = Process(os.getpid())
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
return proc.parent().pid
|
|
29
|
+
except TypeError:
|
|
30
|
+
return proc.parent.pid
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_not_configured_usage_tracker_path():
|
|
34
|
+
"""Returns path of special file where we store latest shell pid."""
|
|
35
|
+
return Path(gettempdir()).joinpath(u'thefuck.last_not_configured_run_{}'.format(
|
|
36
|
+
getpass.getuser(),
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _record_first_run():
|
|
41
|
+
"""Records shell pid to tracker file."""
|
|
42
|
+
info = {'pid': _get_shell_pid(),
|
|
43
|
+
'time': time.time()}
|
|
44
|
+
|
|
45
|
+
mode = 'wb' if six.PY2 else 'w'
|
|
46
|
+
with _get_not_configured_usage_tracker_path().open(mode) as tracker:
|
|
47
|
+
json.dump(info, tracker)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_previous_command():
|
|
51
|
+
history = shell.get_history()
|
|
52
|
+
|
|
53
|
+
if history:
|
|
54
|
+
return history[-1]
|
|
55
|
+
else:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_second_run():
|
|
60
|
+
"""Returns `True` when we know that `fuck` called second time."""
|
|
61
|
+
tracker_path = _get_not_configured_usage_tracker_path()
|
|
62
|
+
if not tracker_path.exists():
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
current_pid = _get_shell_pid()
|
|
66
|
+
with tracker_path.open('r') as tracker:
|
|
67
|
+
try:
|
|
68
|
+
info = json.load(tracker)
|
|
69
|
+
except ValueError:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
if not (isinstance(info, dict) and info.get('pid') == current_pid):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
return (_get_previous_command() == 'fuck' or
|
|
76
|
+
time.time() - info.get('time', 0) < const.CONFIGURATION_TIMEOUT)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_already_configured(configuration_details):
|
|
80
|
+
"""Returns `True` when alias already in shell config."""
|
|
81
|
+
path = Path(configuration_details.path).expanduser()
|
|
82
|
+
with path.open('r') as shell_config:
|
|
83
|
+
return configuration_details.content in shell_config.read()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _configure(configuration_details):
|
|
87
|
+
"""Adds alias to shell config."""
|
|
88
|
+
path = Path(configuration_details.path).expanduser()
|
|
89
|
+
with path.open('a') as shell_config:
|
|
90
|
+
shell_config.write(u'\n')
|
|
91
|
+
shell_config.write(configuration_details.content)
|
|
92
|
+
shell_config.write(u'\n')
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_last_command_from_history():
|
|
96
|
+
"""Gets the last command from shell history, excluding 'fuck' itself."""
|
|
97
|
+
history = shell.get_history()
|
|
98
|
+
if not history:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
# Find the last command that isn't 'fuck'
|
|
102
|
+
for cmd in reversed(history):
|
|
103
|
+
cmd_stripped = cmd.strip()
|
|
104
|
+
if cmd_stripped and cmd_stripped != 'fuck' and not cmd_stripped.startswith('fuck '):
|
|
105
|
+
return cmd_stripped
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _run_and_fix_command(command_script):
|
|
110
|
+
"""Runs the command and tries to fix it."""
|
|
111
|
+
try:
|
|
112
|
+
command = types.Command.from_raw_script([command_script])
|
|
113
|
+
except EmptyCommand:
|
|
114
|
+
logs.debug('Empty command, nothing to do')
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
corrected_commands = get_corrected_commands(command)
|
|
118
|
+
|
|
119
|
+
# Get the first corrected command
|
|
120
|
+
selected_command = None
|
|
121
|
+
for cmd in corrected_commands:
|
|
122
|
+
selected_command = cmd
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
if selected_command:
|
|
126
|
+
# Print the fixed command
|
|
127
|
+
fixed_script = selected_command.script
|
|
128
|
+
print(u'\nSuggested fix: {}'.format(fixed_script))
|
|
129
|
+
|
|
130
|
+
# Ask for confirmation
|
|
131
|
+
try:
|
|
132
|
+
if six.PY2:
|
|
133
|
+
response = raw_input('Run this command? [Y/n] ') # noqa: F821
|
|
134
|
+
else:
|
|
135
|
+
response = input('Run this command? [Y/n] ')
|
|
136
|
+
if response.lower() in ('', 'y', 'yes'):
|
|
137
|
+
subprocess.call(fixed_script, shell=True)
|
|
138
|
+
return True
|
|
139
|
+
except (KeyboardInterrupt, EOFError):
|
|
140
|
+
print('')
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _auto_configure_shell():
|
|
146
|
+
"""Automatically configures the shell if possible."""
|
|
147
|
+
configuration_details = shell.how_to_configure()
|
|
148
|
+
if (configuration_details and
|
|
149
|
+
configuration_details.can_configure_automatically and
|
|
150
|
+
not _is_already_configured(configuration_details)):
|
|
151
|
+
_configure(configuration_details)
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
"""Main entry point for 'fuck' command.
|
|
158
|
+
|
|
159
|
+
This now works in two modes:
|
|
160
|
+
1. If shell alias is configured: works through the alias (fast mode)
|
|
161
|
+
2. If not configured: reads history, reruns command, and fixes it
|
|
162
|
+
|
|
163
|
+
Also auto-configures the shell for better experience next time.
|
|
164
|
+
"""
|
|
165
|
+
settings.init()
|
|
166
|
+
|
|
167
|
+
# Check if we're being called through the alias (TF_COMMAND is set)
|
|
168
|
+
if os.environ.get('TF_COMMAND'):
|
|
169
|
+
# We're being called through alias, delegate to fix_command
|
|
170
|
+
from .fix_command import fix_command
|
|
171
|
+
from ..argument_parser import Parser
|
|
172
|
+
parser = Parser()
|
|
173
|
+
known_args = parser.parse(sys.argv)
|
|
174
|
+
fix_command(known_args)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Not called through alias - work directly with history
|
|
178
|
+
last_command = _get_last_command_from_history()
|
|
179
|
+
|
|
180
|
+
if not last_command:
|
|
181
|
+
logs.how_to_configure_alias(shell.how_to_configure())
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Try to fix the command
|
|
185
|
+
print(u'Last command: {}'.format(last_command))
|
|
186
|
+
print(u'Re-running to get output...')
|
|
187
|
+
|
|
188
|
+
if _run_and_fix_command(last_command):
|
|
189
|
+
# Auto-configure shell for next time
|
|
190
|
+
if _auto_configure_shell():
|
|
191
|
+
print(u'\nShell configured! Restart your terminal for faster experience.')
|
|
192
|
+
else:
|
|
193
|
+
print(u'No fix found for this command.')
|
|
194
|
+
|
|
195
|
+
# Show configuration help
|
|
196
|
+
configuration_details = shell.how_to_configure()
|
|
197
|
+
if configuration_details:
|
|
198
|
+
if _auto_configure_shell():
|
|
199
|
+
print(u'\nShell auto-configured! Restart your terminal.')
|
|
200
|
+
else:
|
|
201
|
+
logs.how_to_configure_alias(configuration_details)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
from getpass import getpass
|
|
5
|
+
import six
|
|
6
|
+
from ..conf import settings
|
|
7
|
+
from ..logs import warn
|
|
8
|
+
from ..shells import shell
|
|
9
|
+
from ..system import Path
|
|
10
|
+
|
|
11
|
+
# Use builtin input to avoid colorama wrapper issues
|
|
12
|
+
if six.PY2:
|
|
13
|
+
_input = raw_input # noqa: F821
|
|
14
|
+
else:
|
|
15
|
+
import builtins
|
|
16
|
+
_input = builtins.input
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ask(prompt, default=None, secret=False):
|
|
20
|
+
if default:
|
|
21
|
+
prompt_text = '{} [{}]: '.format(prompt, default)
|
|
22
|
+
else:
|
|
23
|
+
prompt_text = '{}: '.format(prompt)
|
|
24
|
+
if secret:
|
|
25
|
+
value = getpass(prompt_text)
|
|
26
|
+
else:
|
|
27
|
+
value = _input(prompt_text)
|
|
28
|
+
value = value.strip()
|
|
29
|
+
if not value:
|
|
30
|
+
return default
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ask_bool(prompt, default):
|
|
35
|
+
suffix = 'Y/n' if default else 'y/N'
|
|
36
|
+
while True:
|
|
37
|
+
value = _input('{} ({})? '.format(prompt, suffix)).strip().lower()
|
|
38
|
+
if not value:
|
|
39
|
+
return default
|
|
40
|
+
if value in ('y', 'yes'):
|
|
41
|
+
return True
|
|
42
|
+
if value in ('n', 'no'):
|
|
43
|
+
return False
|
|
44
|
+
sys.stderr.write('Please enter y or n.\n')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ask_int(prompt, default):
|
|
48
|
+
value = _ask(prompt, six.text_type(default))
|
|
49
|
+
try:
|
|
50
|
+
return int(value)
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
warn('Invalid number, using default {}'.format(default))
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _normalize_mode(mode, default):
|
|
57
|
+
if mode in ('prefer', 'fallback'):
|
|
58
|
+
return mode
|
|
59
|
+
warn("Unknown AI mode '{}', using {}".format(mode, default))
|
|
60
|
+
return default
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _quote(value):
|
|
64
|
+
return shell.quote(six.text_type(value))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_fish():
|
|
68
|
+
return shell.__class__.__name__.lower() == 'fish'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _build_env_lines(values, fish):
|
|
72
|
+
lines = []
|
|
73
|
+
for key, value in values.items():
|
|
74
|
+
if value is None:
|
|
75
|
+
continue
|
|
76
|
+
if fish:
|
|
77
|
+
lines.append('set -gx {} {}'.format(key, _quote(value)))
|
|
78
|
+
else:
|
|
79
|
+
lines.append('export {}={}'.format(key, _quote(value)))
|
|
80
|
+
return lines
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _shell_path(path, xdg_config_home, use_xdg):
|
|
84
|
+
if use_xdg and xdg_config_home:
|
|
85
|
+
xdg_root = Path(xdg_config_home, 'thefuck').expanduser()
|
|
86
|
+
if path == xdg_root:
|
|
87
|
+
return '$XDG_CONFIG_HOME/thefuck'
|
|
88
|
+
if str(path).startswith(str(xdg_root) + os.sep):
|
|
89
|
+
suffix = str(path)[len(str(xdg_root)) + 1:]
|
|
90
|
+
return '$XDG_CONFIG_HOME/thefuck/{}'.format(suffix)
|
|
91
|
+
home = Path('~').expanduser()
|
|
92
|
+
if str(path).startswith(str(home) + os.sep):
|
|
93
|
+
suffix = str(path)[len(str(home)) + 1:]
|
|
94
|
+
return '$HOME/{}'.format(suffix)
|
|
95
|
+
return str(path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _write_env_file(path, lines):
|
|
99
|
+
with path.open(mode='w') as env_file:
|
|
100
|
+
env_file.write('# thefuck env\n')
|
|
101
|
+
for line in lines:
|
|
102
|
+
env_file.write(line + '\n')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_path_lines(bin_path, fish):
|
|
106
|
+
if fish:
|
|
107
|
+
return ['set -gx PATH {} $PATH'.format(bin_path)]
|
|
108
|
+
return ['export PATH="{}:$PATH"'.format(bin_path)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_wrapper(path):
|
|
112
|
+
content = (
|
|
113
|
+
'#!/bin/sh\n'
|
|
114
|
+
'if [ -n "$THEFUCK_PROJECT_PATH" ]; then\n'
|
|
115
|
+
' exec uv run --project "$THEFUCK_PROJECT_PATH" thefuck "$@"\n'
|
|
116
|
+
'fi\n'
|
|
117
|
+
'if command -v uvx >/dev/null 2>&1; then\n'
|
|
118
|
+
' exec uvx thefuck "$@"\n'
|
|
119
|
+
'fi\n'
|
|
120
|
+
'exec uv run thefuck "$@"\n'
|
|
121
|
+
)
|
|
122
|
+
with path.open(mode='w') as script_file:
|
|
123
|
+
script_file.write(content)
|
|
124
|
+
os.chmod(str(path), 0o755)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def setup():
|
|
128
|
+
print('Setup:')
|
|
129
|
+
print('Press enter to keep the default value shown in brackets.')
|
|
130
|
+
|
|
131
|
+
xdg_config_home_env = os.environ.get('XDG_CONFIG_HOME')
|
|
132
|
+
xdg_config_home = xdg_config_home_env or '~/.config'
|
|
133
|
+
use_xdg = xdg_config_home_env is not None
|
|
134
|
+
config_root = Path(xdg_config_home, 'thefuck').expanduser()
|
|
135
|
+
legacy_root = Path('~/.thefuck').expanduser()
|
|
136
|
+
if legacy_root.is_dir() and legacy_root != config_root:
|
|
137
|
+
if _ask_bool(
|
|
138
|
+
'Legacy config found at {}. Migrate to {}'
|
|
139
|
+
.format(legacy_root, config_root),
|
|
140
|
+
True):
|
|
141
|
+
try:
|
|
142
|
+
if not config_root.parent.is_dir():
|
|
143
|
+
config_root.parent.mkdir(parents=True)
|
|
144
|
+
shutil.move(str(legacy_root), str(config_root))
|
|
145
|
+
except OSError as exc:
|
|
146
|
+
warn('Failed to migrate config: {}'.format(exc))
|
|
147
|
+
config_root = legacy_root
|
|
148
|
+
else:
|
|
149
|
+
config_root = legacy_root
|
|
150
|
+
bin_dir = config_root.joinpath('bin')
|
|
151
|
+
fish = _is_fish()
|
|
152
|
+
env_path = config_root.joinpath('env.fish' if fish else 'env.sh')
|
|
153
|
+
env_path_shell = _shell_path(env_path, xdg_config_home, use_xdg)
|
|
154
|
+
bin_path_shell = _shell_path(bin_dir, xdg_config_home, use_xdg)
|
|
155
|
+
|
|
156
|
+
if not config_root.is_dir():
|
|
157
|
+
config_root.mkdir(parents=True)
|
|
158
|
+
if not bin_dir.is_dir():
|
|
159
|
+
bin_dir.mkdir(parents=True)
|
|
160
|
+
|
|
161
|
+
settings.init()
|
|
162
|
+
defaults = {
|
|
163
|
+
'ai_enabled': settings.ai_enabled,
|
|
164
|
+
'ai_url': settings.ai_url,
|
|
165
|
+
'ai_token': settings.ai_token,
|
|
166
|
+
'ai_model': settings.ai_model,
|
|
167
|
+
'ai_timeout': settings.ai_timeout,
|
|
168
|
+
'ai_reasoning_effort': settings.ai_reasoning_effort,
|
|
169
|
+
'ai_stream': settings.ai_stream,
|
|
170
|
+
'ai_mode': settings.ai_mode,
|
|
171
|
+
'ai_stream_output': settings.ai_stream_output
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ai_enabled = _ask_bool('Enable AI', defaults['ai_enabled'])
|
|
175
|
+
values = {
|
|
176
|
+
'THEFUCK_AI_ENABLED': 'true' if ai_enabled else 'false'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ai_enabled:
|
|
180
|
+
ai_url = _ask('AI URL', defaults['ai_url'])
|
|
181
|
+
ai_token = _ask('AI token', defaults['ai_token'], secret=False)
|
|
182
|
+
ai_model = _ask('AI model', defaults['ai_model'])
|
|
183
|
+
ai_timeout = _ask_int('Timeout seconds', defaults['ai_timeout'])
|
|
184
|
+
ai_reasoning_effort = _ask(
|
|
185
|
+
'Reasoning effort (low/medium/high)',
|
|
186
|
+
defaults['ai_reasoning_effort'])
|
|
187
|
+
ai_stream = _ask_bool('Use SSE stream', defaults['ai_stream'])
|
|
188
|
+
ai_stream_output = _ask_bool(
|
|
189
|
+
'Stream output while waiting', defaults['ai_stream_output'])
|
|
190
|
+
ai_mode = _normalize_mode(
|
|
191
|
+
_ask('AI mode (prefer/fallback)', defaults['ai_mode']),
|
|
192
|
+
defaults['ai_mode'])
|
|
193
|
+
|
|
194
|
+
values.update({
|
|
195
|
+
'THEFUCK_AI_URL': ai_url,
|
|
196
|
+
'THEFUCK_AI_TOKEN': ai_token,
|
|
197
|
+
'THEFUCK_AI_MODEL': ai_model,
|
|
198
|
+
'THEFUCK_AI_TIMEOUT': ai_timeout,
|
|
199
|
+
'THEFUCK_AI_REASONING_EFFORT': ai_reasoning_effort,
|
|
200
|
+
'THEFUCK_AI_STREAM': 'true' if ai_stream else 'false',
|
|
201
|
+
'THEFUCK_AI_MODE': ai_mode,
|
|
202
|
+
'THEFUCK_AI_STREAM_OUTPUT': 'true' if ai_stream_output else 'false'
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
env_lines = _build_path_lines(bin_path_shell, fish)
|
|
206
|
+
env_lines.extend(_build_env_lines(values, fish))
|
|
207
|
+
_write_env_file(env_path, env_lines)
|
|
208
|
+
_write_wrapper(bin_dir.joinpath('thefuck'))
|
|
209
|
+
|
|
210
|
+
print('\nWrote env file:', env_path)
|
|
211
|
+
print('Wrote wrapper:', bin_dir.joinpath('thefuck'))
|
|
212
|
+
print('\nAdd to your shell config:')
|
|
213
|
+
print('source {}'.format(env_path_shell))
|
|
214
|
+
print('\nOptional for local development:')
|
|
215
|
+
print('set THEFUCK_PROJECT_PATH to use a local checkout with uv run')
|
|
216
|
+
|
|
217
|
+
config = shell.how_to_configure()
|
|
218
|
+
if config and _ask_bool(
|
|
219
|
+
'Append setup to {}'.format(config.path), False):
|
|
220
|
+
path = os.path.expanduser(config.path)
|
|
221
|
+
try:
|
|
222
|
+
with open(path, 'a') as config_file:
|
|
223
|
+
config_file.write('\n# thefuck config\n')
|
|
224
|
+
config_file.write('source {}\n'.format(env_path_shell))
|
|
225
|
+
print('Done. Run: {}'.format(config.reload))
|
|
226
|
+
except OSError as exc:
|
|
227
|
+
warn('Failed to write {}: {}'.format(path, exc))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import array
|
|
2
|
+
import fcntl
|
|
3
|
+
from functools import partial
|
|
4
|
+
import mmap
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import termios
|
|
10
|
+
import tty
|
|
11
|
+
from .. import logs, const
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read(f, fd):
|
|
15
|
+
data = os.read(fd, 1024)
|
|
16
|
+
try:
|
|
17
|
+
f.write(data)
|
|
18
|
+
except ValueError:
|
|
19
|
+
position = const.LOG_SIZE_IN_BYTES - const.LOG_SIZE_TO_CLEAN
|
|
20
|
+
f.move(0, const.LOG_SIZE_TO_CLEAN, position)
|
|
21
|
+
f.seek(position)
|
|
22
|
+
f.write(b'\x00' * const.LOG_SIZE_TO_CLEAN)
|
|
23
|
+
f.seek(position)
|
|
24
|
+
return data
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _set_pty_size(master_fd):
|
|
28
|
+
buf = array.array('h', [0, 0, 0, 0])
|
|
29
|
+
fcntl.ioctl(pty.STDOUT_FILENO, termios.TIOCGWINSZ, buf, True)
|
|
30
|
+
fcntl.ioctl(master_fd, termios.TIOCSWINSZ, buf)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _spawn(shell, master_read):
|
|
34
|
+
"""Create a spawned process.
|
|
35
|
+
|
|
36
|
+
Modified version of pty.spawn with terminal size support.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
pid, master_fd = pty.fork()
|
|
40
|
+
|
|
41
|
+
if pid == pty.CHILD:
|
|
42
|
+
os.execlp(shell, shell)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
mode = tty.tcgetattr(pty.STDIN_FILENO)
|
|
46
|
+
tty.setraw(pty.STDIN_FILENO)
|
|
47
|
+
restore = True
|
|
48
|
+
except tty.error: # This is the same as termios.error
|
|
49
|
+
restore = False
|
|
50
|
+
|
|
51
|
+
_set_pty_size(master_fd)
|
|
52
|
+
signal.signal(signal.SIGWINCH, lambda *_: _set_pty_size(master_fd))
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
pty._copy(master_fd, master_read, pty._read)
|
|
56
|
+
except OSError:
|
|
57
|
+
if restore:
|
|
58
|
+
tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
|
|
59
|
+
|
|
60
|
+
os.close(master_fd)
|
|
61
|
+
return os.waitpid(pid, 0)[1]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def shell_logger(output):
|
|
65
|
+
"""Logs shell output to the `output`.
|
|
66
|
+
|
|
67
|
+
Works like unix script command with `-f` flag.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if not os.environ.get('SHELL'):
|
|
71
|
+
logs.warn("Shell logger doesn't support your platform.")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
fd = os.open(output, os.O_CREAT | os.O_TRUNC | os.O_RDWR)
|
|
75
|
+
os.write(fd, b'\x00' * const.LOG_SIZE_IN_BYTES)
|
|
76
|
+
buffer = mmap.mmap(fd, const.LOG_SIZE_IN_BYTES, mmap.MAP_SHARED, mmap.PROT_WRITE)
|
|
77
|
+
return_code = _spawn(os.environ['SHELL'], partial(_read, buffer))
|
|
78
|
+
|
|
79
|
+
sys.exit(return_code)
|
thefuck/exceptions.py
ADDED