0
# Error Handling
1
2
Robust error handling system with specific exception classes for different failure modes, exit code management, and comprehensive error information capture. Provides detailed debugging information and flexible error handling strategies.
3
4
## Capabilities
5
6
### Exception Hierarchy
7
8
Comprehensive exception system with specific classes for different types of command failures.
9
10
```python { .api }
11
class ErrorReturnCode(Exception):
12
"""
13
Base exception for all command execution errors.
14
15
Attributes:
16
- full_cmd: str = complete command that was executed
17
- stdout: bytes = command's stdout output
18
- stderr: bytes = command's stderr output
19
- truncate_cap: int = output truncation limit
20
"""
21
22
def __init__(self, full_cmd: str, stdout: bytes, stderr: bytes, truncate_cap: int = 750): ...
23
24
@property
25
def exit_code(self) -> int:
26
"""The exit code returned by the command."""
27
28
class ErrorReturnCode_1(ErrorReturnCode):
29
"""Exception for commands that exit with code 1 (general errors)."""
30
31
class ErrorReturnCode_2(ErrorReturnCode):
32
"""Exception for commands that exit with code 2 (misuse of shell builtins)."""
33
34
class ErrorReturnCode_126(ErrorReturnCode):
35
"""Exception for commands that exit with code 126 (command not executable)."""
36
37
class ErrorReturnCode_127(ErrorReturnCode):
38
"""Exception for commands that exit with code 127 (command not found)."""
39
40
class ErrorReturnCode_128(ErrorReturnCode):
41
"""Exception for commands that exit with code 128 (invalid exit argument)."""
42
43
# Note: sh dynamically creates ErrorReturnCode_N classes for exit codes 1-255
44
# Common exit codes include:
45
# 1 - General errors, 2 - Misuse of shell builtins, 126 - Command not executable
46
# 127 - Command not found, 128 - Invalid exit argument, 130 - Script terminated by Ctrl+C
47
48
class SignalException(Exception):
49
"""Exception raised when process is terminated by a signal."""
50
51
def __init__(self, full_cmd: str, signal_code: int): ...
52
53
class TimeoutException(Exception):
54
"""Exception raised when command times out."""
55
56
def __init__(self, full_cmd: str, timeout: float): ...
57
58
class CommandNotFound(Exception):
59
"""Exception raised when command cannot be found in PATH."""
60
61
def __init__(self, command: str): ...
62
63
class ForkException(Exception):
64
"""Exception raised when there's an error in the fork process."""
65
66
def __init__(self, command: str, error: str): ...
67
```
68
69
Usage examples:
70
71
```python
72
import sh
73
74
# Catch specific exit codes
75
try:
76
sh.grep("nonexistent_pattern", "file.txt")
77
except sh.ErrorReturnCode_1:
78
print("Pattern not found (exit code 1)")
79
except sh.ErrorReturnCode_2:
80
print("File not found or other error (exit code 2)")
81
82
# Catch any command error
83
try:
84
result = sh.ls("/nonexistent_directory")
85
except sh.ErrorReturnCode as e:
86
print(f"Command failed: {e.full_cmd}")
87
print(f"Exit code: {e.exit_code}")
88
print(f"Stderr: {e.stderr.decode('utf-8')}")
89
90
# Handle signal termination
91
try:
92
sh.sleep(60, _timeout=5)
93
except sh.TimeoutException as e:
94
print(f"Command timed out after {e.timeout} seconds")
95
except sh.SignalException as e:
96
print(f"Command terminated by signal {e.signal_code}")
97
```
98
99
### Exit Code Management
100
101
Control which exit codes are considered successful and handle non-standard success codes.
102
103
```python { .api }
104
def __call__(self, *args, _ok_code=None, **kwargs):
105
"""
106
Execute command with custom success codes.
107
108
Parameters:
109
- _ok_code: int/list = exit codes to treat as success
110
111
Returns:
112
str: Command output
113
"""
114
```
115
116
Usage examples:
117
118
```python
119
import sh
120
121
# Accept multiple exit codes as success
122
try:
123
# grep returns 0 if found, 1 if not found, 2 for errors
124
result = sh.grep("pattern", "file.txt", _ok_code=[0, 1])
125
if "pattern" in result:
126
print("Pattern found")
127
else:
128
print("Pattern not found (but that's OK)")
129
except sh.ErrorReturnCode:
130
print("Real error occurred (exit code 2)")
131
132
# Accept any exit code
133
output = sh.some_command(_ok_code=list(range(256)))
134
135
# Custom validation logic
136
def validate_exit_code(code, stdout, stderr):
137
"""Custom logic to determine if command succeeded."""
138
if code == 0:
139
return True
140
if code == 1 and "warning" in stderr.decode().lower():
141
return True # Treat warnings as success
142
return False
143
144
# Use with manual error handling
145
proc = sh.custom_tool(_bg=True)
146
proc.wait()
147
148
if validate_exit_code(proc.exit_code, proc.stdout, proc.stderr):
149
print("Command succeeded (with custom logic)")
150
else:
151
print(f"Command failed with exit code {proc.exit_code}")
152
```
153
154
### Error Information Access
155
156
Access detailed error information for debugging and logging.
157
158
```python { .api }
159
class ErrorReturnCode:
160
@property
161
def exit_code(self) -> int:
162
"""Exit code returned by the command."""
163
164
@property
165
def full_cmd(self) -> str:
166
"""Complete command line that was executed."""
167
168
@property
169
def stdout(self) -> bytes:
170
"""Standard output from the failed command."""
171
172
@property
173
def stderr(self) -> bytes:
174
"""Standard error from the failed command."""
175
```
176
177
Usage examples:
178
179
```python
180
import sh
181
import logging
182
183
# Detailed error logging
184
logger = logging.getLogger(__name__)
185
186
try:
187
sh.complex_command("arg1", "arg2", "--option", "value")
188
except sh.ErrorReturnCode as e:
189
logger.error(f"Command failed: {e.full_cmd}")
190
logger.error(f"Exit code: {e.exit_code}")
191
192
# Log stdout if available
193
if e.stdout:
194
logger.info(f"Stdout: {e.stdout.decode('utf-8', errors='replace')}")
195
196
# Log stderr if available
197
if e.stderr:
198
logger.error(f"Stderr: {e.stderr.decode('utf-8', errors='replace')}")
199
200
# Re-raise for higher-level handling
201
raise
202
203
# Error analysis
204
def analyze_error(error):
205
"""Analyze error and suggest solutions."""
206
if isinstance(error, sh.CommandNotFound):
207
return f"Command '{error.command}' not found. Check if it's installed and in PATH."
208
209
if isinstance(error, sh.TimeoutException):
210
return f"Command timed out after {error.timeout} seconds. Consider increasing timeout."
211
212
if isinstance(error, sh.ErrorReturnCode):
213
stderr_text = error.stderr.decode('utf-8', errors='replace').lower()
214
215
if "permission denied" in stderr_text:
216
return "Permission denied. Try running with sudo or check file permissions."
217
elif "no such file" in stderr_text:
218
return "File or directory not found. Check the path."
219
elif "command not found" in stderr_text:
220
return "Command not found in PATH."
221
else:
222
return f"Command failed with exit code {error.exit_code}"
223
224
return "Unknown error occurred."
225
226
try:
227
sh.restricted_command()
228
except Exception as e:
229
suggestion = analyze_error(e)
230
print(f"Error: {suggestion}")
231
```
232
233
### Error Recovery and Retry
234
235
Implement retry logic and error recovery strategies.
236
237
```python
238
import sh
239
import time
240
import random
241
242
def retry_command(command_func, max_retries=3, backoff_factor=1.0):
243
"""
244
Retry a command with exponential backoff.
245
246
Parameters:
247
- command_func: callable that executes the sh command
248
- max_retries: int = maximum number of retry attempts
249
- backoff_factor: float = multiplier for retry delay
250
251
Returns:
252
str: Command output if successful
253
254
Raises:
255
Exception: Last exception if all retries fail
256
"""
257
last_exception = None
258
259
for attempt in range(max_retries + 1):
260
try:
261
return command_func()
262
except (sh.ErrorReturnCode, sh.TimeoutException) as e:
263
last_exception = e
264
265
if attempt < max_retries:
266
delay = (backoff_factor * (2 ** attempt)) + random.uniform(0, 1)
267
print(f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s...")
268
time.sleep(delay)
269
else:
270
print(f"All {max_retries + 1} attempts failed")
271
272
raise last_exception
273
274
# Usage examples
275
def flaky_network_command():
276
return sh.curl("http://unreliable-server.com/api", _timeout=10)
277
278
try:
279
result = retry_command(flaky_network_command, max_retries=3, backoff_factor=2.0)
280
print("Command succeeded:", result[:100])
281
except Exception as e:
282
print(f"Command failed after retries: {e}")
283
284
# Selective retry based on error type
285
def smart_retry_command(command_func, max_retries=3):
286
"""Retry only for certain types of errors."""
287
retryable_errors = (sh.TimeoutException, sh.ErrorReturnCode_124) # timeout error codes
288
289
for attempt in range(max_retries + 1):
290
try:
291
return command_func()
292
except retryable_errors as e:
293
if attempt < max_retries:
294
print(f"Retryable error on attempt {attempt + 1}: {e}")
295
time.sleep(2 ** attempt) # Exponential backoff
296
else:
297
raise
298
except Exception as e:
299
# Don't retry for non-retryable errors
300
print(f"Non-retryable error: {e}")
301
raise
302
303
# Fallback command strategy
304
def command_with_fallback(primary_cmd, fallback_cmd):
305
"""Try primary command, fall back to alternative if it fails."""
306
try:
307
return primary_cmd()
308
except sh.CommandNotFound:
309
print("Primary command not found, trying fallback...")
310
return fallback_cmd()
311
except sh.ErrorReturnCode as e:
312
if e.exit_code in [126, 127]: # Permission or not found
313
print("Primary command failed, trying fallback...")
314
return fallback_cmd()
315
else:
316
raise
317
318
# Usage
319
result = command_with_fallback(
320
lambda: sh.gls("-la"), # GNU ls
321
lambda: sh.ls("-la") # BSD ls fallback
322
)
323
```
324
325
### Custom Error Handling
326
327
Create custom error handling patterns for specific use cases.
328
329
```python
330
import sh
331
from contextlib import contextmanager
332
333
@contextmanager
334
def ignore_errors(*error_types):
335
"""Context manager to ignore specific error types."""
336
try:
337
yield
338
except error_types as e:
339
print(f"Ignoring error: {e}")
340
341
@contextmanager
342
def log_errors(logger):
343
"""Context manager to log but not raise errors."""
344
try:
345
yield
346
except Exception as e:
347
logger.error(f"Command error: {e}")
348
# Don't re-raise, just log
349
350
# Usage examples
351
import logging
352
logger = logging.getLogger(__name__)
353
354
# Ignore file not found errors
355
with ignore_errors(sh.ErrorReturnCode_2):
356
sh.rm("nonexistent_file.txt")
357
print("File deletion attempted (may not have existed)")
358
359
# Log errors but continue
360
with log_errors(logger):
361
sh.risky_command()
362
print("Risky command attempted")
363
364
# Custom error context
365
class CommandContext:
366
def __init__(self, description):
367
self.description = description
368
369
def __enter__(self):
370
return self
371
372
def __exit__(self, exc_type, exc_val, exc_tb):
373
if exc_type and issubclass(exc_type, sh.ErrorReturnCode):
374
print(f"Error in {self.description}: {exc_val}")
375
return True # Suppress the exception
376
return False
377
378
# Usage
379
with CommandContext("database backup"):
380
sh.mysqldump("--all-databases")
381
382
with CommandContext("log rotation"):
383
sh.logrotate("/etc/logrotate.conf")
384
```