0
# Rule Development
1
2
Creating custom correction rules, rule structure, and integration patterns. This guide enables extending the correction system with new command-specific logic to handle additional CLI tools and error patterns not covered by built-in rules.
3
4
## Capabilities
5
6
### Rule Function Requirements
7
8
Every rule module must implement these core functions for integration with the correction system.
9
10
```python { .api }
11
def match(command, settings):
12
"""
13
Determines if this rule applies to the given command.
14
15
Parameters:
16
- command (Command): The failed command with script, stdout, stderr
17
- settings (Settings): Application settings and configuration
18
19
Returns:
20
bool: True if this rule can provide corrections for the command
21
22
This function should analyze the command to determine applicability.
23
Common patterns:
24
- Check command.script for command name or arguments
25
- Check command.stderr for specific error messages
26
- Check command.stdout for output patterns
27
- Use settings for rule configuration
28
"""
29
30
def get_new_command(command, settings):
31
"""
32
Generates corrected command(s) for the matched command.
33
34
Parameters:
35
- command (Command): The failed command to correct
36
- settings (Settings): Application settings and configuration
37
38
Returns:
39
str or list[str]: Single correction or list of possible corrections
40
41
This function should return the corrected command string(s).
42
Multiple corrections are ranked by order in the list.
43
"""
44
```
45
46
### Optional Rule Attributes
47
48
Rules can define optional attributes to customize their behavior and integration.
49
50
```python { .api }
51
enabled_by_default = True
52
"""
53
bool: Whether this rule should be enabled by default.
54
55
Default: True if not specified
56
Set to False for experimental rules or rules that may conflict
57
"""
58
59
priority = 1000
60
"""
61
int: Rule priority for ordering corrections.
62
63
Lower numbers = higher priority (executed first)
64
Default: conf.DEFAULT_PRIORITY (1000) if not specified
65
66
Priority guidelines:
67
- 100-500: Critical fixes (sudo, permission issues)
68
- 500-1000: Common command corrections
69
- 1000-1500: Typo fixes and suggestions
70
- 1500+: Experimental or low-confidence corrections
71
"""
72
73
requires_output = True
74
"""
75
bool: Whether this rule needs command output to function.
76
77
Default: True if not specified
78
Set to False for rules that only analyze command.script
79
80
Rules with requires_output=False can work with:
81
- Commands that produce no output
82
- Timeouts or killed processes
83
- Script-only analysis
84
"""
85
86
def side_effect(old_cmd, new_cmd, settings):
87
"""
88
Optional function executed when the corrected command runs.
89
90
Parameters:
91
- old_cmd (Command): Original failed command
92
- new_cmd (str): Corrected command being executed
93
- settings (Settings): Application settings
94
95
Returns:
96
None
97
98
Use for additional actions like:
99
- Updating configuration files
100
- Creating directories or files
101
- Logging corrections
102
- Cleaning up state
103
"""
104
```
105
106
## Rule Development Patterns
107
108
### Basic Command Name Matching
109
110
Simple rules that match based on command name or structure.
111
112
```python
113
# Example: Fix 'gti' typo to 'git'
114
def match(command, settings):
115
return command.script.startswith('gti ')
116
117
def get_new_command(command, settings):
118
return command.script.replace('gti ', 'git ', 1)
119
120
enabled_by_default = True
121
priority = 900
122
requires_output = False
123
```
124
125
### Error Message Pattern Matching
126
127
Rules that analyze error output to determine corrections.
128
129
```python
130
# Example: Add sudo for permission denied errors
131
def match(command, settings):
132
return ('permission denied' in command.stderr.lower() or
133
'operation not permitted' in command.stderr.lower())
134
135
def get_new_command(command, settings):
136
return f'sudo {command.script}'
137
138
enabled_by_default = True
139
priority = 100 # High priority for permission fixes
140
```
141
142
### Complex Analysis with Multiple Corrections
143
144
Advanced rules that provide multiple correction options.
145
146
```python
147
import re
148
from thefuck.utils import get_closest
149
150
def match(command, settings):
151
return (command.script.startswith('git ') and
152
'is not a git command' in command.stderr)
153
154
def get_new_command(command, settings):
155
# Extract the invalid git command
156
match = re.search(r"git: '(\w+)' is not a git command", command.stderr)
157
if not match:
158
return []
159
160
invalid_cmd = match.group(1)
161
git_commands = ['push', 'pull', 'commit', 'checkout', 'branch', 'merge', 'status']
162
163
# Find similar commands
164
closest = get_closest(invalid_cmd, git_commands, cutoff=0.6)
165
if closest:
166
base_cmd = command.script.replace(invalid_cmd, closest, 1)
167
return [base_cmd]
168
169
return []
170
171
enabled_by_default = True
172
priority = 1000
173
```
174
175
### Application-Specific Rules with Decorators
176
177
Rules using utility decorators for cleaner code.
178
179
```python
180
from thefuck.utils import for_app
181
from thefuck.specific.sudo import sudo_support
182
183
@sudo_support
184
@for_app('docker')
185
def match(command, settings):
186
return 'permission denied' in command.stderr.lower()
187
188
@sudo_support
189
def get_new_command(command, settings):
190
# sudo_support decorator automatically adds/removes sudo
191
return command.script
192
193
enabled_by_default = True
194
priority = 200
195
```
196
197
### Rules with Side Effects
198
199
Rules that perform additional actions when corrections are executed.
200
201
```python
202
import os
203
from pathlib import Path
204
205
def match(command, settings):
206
return (command.script.startswith('cd ') and
207
'no such file or directory' in command.stderr.lower())
208
209
def get_new_command(command, settings):
210
# Extract the directory name
211
parts = command.script.split()
212
if len(parts) > 1:
213
directory = parts[1]
214
return f'mkdir -p {directory} && cd {directory}'
215
return command.script
216
217
def side_effect(old_cmd, new_cmd, settings):
218
"""Log directory creation for future reference."""
219
parts = new_cmd.split('&&')[0].strip().split()
220
if len(parts) > 2:
221
created_dir = parts[2]
222
log_file = Path.home() / '.thefuck' / 'created_dirs.log'
223
with log_file.open('a') as f:
224
f.write(f"{created_dir}\n")
225
226
enabled_by_default = True
227
priority = 1200
228
```
229
230
## Advanced Rule Patterns
231
232
### Multi-Application Rules
233
234
Rules that handle multiple related applications.
235
236
```python
237
def match(command, settings):
238
package_managers = ['apt-get', 'yum', 'dnf', 'pacman']
239
return (any(command.script.startswith(pm) for pm in package_managers) and
240
'permission denied' in command.stderr.lower())
241
242
def get_new_command(command, settings):
243
return f'sudo {command.script}'
244
245
enabled_by_default = True
246
priority = 150
247
```
248
249
### Context-Aware Rules
250
251
Rules that use environment context for better corrections.
252
253
```python
254
import os
255
from thefuck.utils import which
256
257
def match(command, settings):
258
return (command.script.startswith('python ') and
259
'command not found' in command.stderr)
260
261
def get_new_command(command, settings):
262
corrections = []
263
264
# Try python3 if available
265
if which('python3'):
266
corrections.append(command.script.replace('python ', 'python3 ', 1))
267
268
# Try py if on Windows
269
if os.name == 'nt' and which('py'):
270
corrections.append(command.script.replace('python ', 'py ', 1))
271
272
return corrections
273
274
enabled_by_default = True
275
priority = 800
276
```
277
278
### History-Based Rules
279
280
Rules that use command history for intelligent corrections.
281
282
```python
283
from thefuck.utils import get_valid_history_without_current
284
285
def match(command, settings):
286
return 'command not found' in command.stderr
287
288
def get_new_command(command, settings):
289
history = get_valid_history_without_current(command)
290
291
# Look for similar commands in history
292
invalid_cmd = command.script.split()[0]
293
for hist_cmd in history[:20]: # Check recent history
294
if hist_cmd.startswith(invalid_cmd[:-1]): # Similar prefix
295
return hist_cmd
296
297
return []
298
299
enabled_by_default = False # Experimental
300
priority = 1800
301
```
302
303
## Rule Installation and Testing
304
305
### User Rule Location
306
307
Custom rules should be placed in the user's rules directory:
308
309
```
310
~/.thefuck/rules/my_custom_rule.py
311
```
312
313
### Rule Testing Template
314
315
```python
316
# Template for testing custom rules
317
from thefuck.types import Command
318
319
def test_rule():
320
"""Test the custom rule functionality."""
321
# Test command that should match
322
test_command = Command(
323
script="my_command --flag",
324
stdout="",
325
stderr="my_command: error message"
326
)
327
328
# Test match function
329
assert match(test_command, {}) == True
330
331
# Test correction generation
332
correction = get_new_command(test_command, {})
333
assert correction == "expected_correction"
334
335
print("Rule tests passed!")
336
337
if __name__ == "__main__":
338
test_rule()
339
```
340
341
### Debugging Rules
342
343
```python
344
from thefuck import logs
345
346
def match(command, settings):
347
logs.debug(f"Testing rule against: {command.script}", settings)
348
349
result = # ... rule logic ...
350
351
logs.debug(f"Rule match result: {result}", settings)
352
return result
353
354
def get_new_command(command, settings):
355
logs.debug(f"Generating correction for: {command.script}", settings)
356
357
correction = # ... correction logic ...
358
359
logs.debug(f"Generated correction: {correction}", settings)
360
return correction
361
```
362
363
## Best Practices
364
365
### Rule Design Guidelines
366
367
1. **Specificity**: Make match conditions as specific as possible to avoid false positives
368
2. **Performance**: Keep match functions fast - they're called frequently
369
3. **Robustness**: Handle edge cases and malformed commands gracefully
370
4. **Testing**: Test rules with various command variations and error messages
371
5. **Documentation**: Include clear docstrings explaining rule purpose and behavior
372
373
### Error Handling
374
375
```python
376
def match(command, settings):
377
try:
378
# Rule logic that might fail
379
return analyze_command(command)
380
except Exception as e:
381
# Log error but don't crash
382
logs.exception(f"Rule match error: {e}", settings)
383
return False
384
385
def get_new_command(command, settings):
386
try:
387
# Correction logic
388
return generate_correction(command)
389
except Exception as e:
390
logs.exception(f"Rule correction error: {e}", settings)
391
return [] # Return empty list if correction fails
392
```
393
394
### Integration Patterns
395
396
```python
397
# Use existing utilities for common operations
398
from thefuck.utils import replace_argument, get_closest, for_app
399
from thefuck.specific.sudo import sudo_support
400
401
# Combine decorators for powerful rule behavior
402
@sudo_support
403
@for_app('git')
404
def match(command, settings):
405
return 'permission denied' in command.stderr
406
407
@sudo_support # This will automatically handle sudo addition/removal
408
def get_new_command(command, settings):
409
return command.script # sudo_support handles the rest
410
```
411
412
This comprehensive rule development system allows users to extend thefuck's capabilities to handle any command-line tool or error pattern, making it a truly extensible correction system.