or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

command-execution.mdcontrib.mderror-handling.mdindex.mdio-handling.mdprocess-management.mdutilities.md

error-handling.mddocs/

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

```