0
# Utilities
1
2
Additional utilities including directory manipulation, enhanced globbing, logging, and command introspection tools. These utilities enhance the core sh functionality with convenient helpers for common shell integration scenarios.
3
4
## Capabilities
5
6
### Directory Context Management
7
8
Change working directory temporarily using context managers, similar to shell pushd/popd functionality.
9
10
```python { .api }
11
@contextmanager
12
def pushd(path):
13
"""
14
Context manager to temporarily change working directory.
15
16
Parameters:
17
- path: str/Path = directory to change to
18
19
Yields:
20
str: The new current working directory
21
22
Raises:
23
OSError: If directory doesn't exist or can't be accessed
24
"""
25
```
26
27
Usage examples:
28
29
```python
30
import sh
31
32
# Temporary directory change
33
original_dir = sh.pwd().strip()
34
print(f"Starting in: {original_dir}")
35
36
with sh.pushd("/tmp"):
37
current = sh.pwd().strip()
38
print(f"Now in: {current}")
39
40
# All commands run in /tmp
41
files = sh.ls("-la")
42
sh.touch("test_file.txt")
43
44
# Automatically back to original directory
45
back_dir = sh.pwd().strip()
46
print(f"Back in: {back_dir}")
47
assert back_dir == original_dir
48
49
# Nested directory changes
50
with sh.pushd("/tmp"):
51
print(f"Level 1: {sh.pwd().strip()}")
52
53
with sh.pushd("subdir"):
54
print(f"Level 2: {sh.pwd().strip()}")
55
sh.touch("nested_file.txt")
56
57
print(f"Back to level 1: {sh.pwd().strip()}")
58
59
print(f"Back to original: {sh.pwd().strip()}")
60
61
# Error handling with pushd
62
try:
63
with sh.pushd("/nonexistent"):
64
print("This won't execute")
65
except OSError as e:
66
print(f"Directory change failed: {e}")
67
```
68
69
### Enhanced Globbing
70
71
Enhanced glob functionality that integrates with sh commands and provides additional features.
72
73
```python { .api }
74
def glob(pattern, *args, **kwargs):
75
"""
76
Enhanced glob function that returns results compatible with sh commands.
77
78
Parameters:
79
- pattern: str = glob pattern (supports *, ?, [], {})
80
- *args: additional patterns to match
81
- **kwargs: glob options
82
83
Returns:
84
GlobResults: List-like object with enhanced functionality
85
"""
86
87
class GlobResults(list):
88
"""Enhanced list of glob results with additional methods."""
89
def __init__(self, results): ...
90
```
91
92
Usage examples:
93
94
```python
95
import sh
96
97
# Basic globbing
98
log_files = sh.glob("/var/log/*.log")
99
print(f"Found {len(log_files)} log files")
100
101
# Use glob results with sh commands
102
for log_file in log_files:
103
size = sh.wc("-l", log_file)
104
print(f"{log_file}: {size.strip()} lines")
105
106
# Multiple patterns
107
config_files = sh.glob("*.conf", "*.cfg", "*.ini")
108
print("Configuration files:", config_files)
109
110
# Complex patterns
111
python_files = sh.glob("**/*.py", recursive=True)
112
test_files = sh.glob("**/test_*.py", "**/tests/*.py")
113
114
# Integration with other commands
115
sh.tar("czf", "backup.tar.gz", *sh.glob("*.txt", "*.md"))
116
117
# Filter glob results
118
large_files = []
119
for file_path in sh.glob("*"):
120
try:
121
size = int(sh.stat("-c", "%s", file_path))
122
if size > 1024 * 1024: # > 1MB
123
large_files.append(file_path)
124
except sh.ErrorReturnCode:
125
pass # Skip files we can't stat
126
127
print(f"Large files: {large_files}")
128
```
129
130
### Command Execution Logging
131
132
Built-in logging system for tracking command execution and debugging.
133
134
```python { .api }
135
class Logger:
136
"""
137
Memory-efficient command execution logger.
138
139
Attributes:
140
- name: str = logger name
141
- context: dict = additional context information
142
"""
143
144
def __init__(self, name: str, context: dict = None): ...
145
146
def log(self, level: str, message: str): ...
147
def info(self, message: str): ...
148
def debug(self, message: str): ...
149
def warning(self, message: str): ...
150
def error(self, message: str): ...
151
```
152
153
```python { .api }
154
def __call__(self, *args, _log=None, **kwargs):
155
"""
156
Execute command with logging.
157
158
Parameters:
159
- _log: Logger/bool = logger instance or True for default logging
160
161
Returns:
162
str: Command output
163
"""
164
```
165
166
Usage examples:
167
168
```python
169
import sh
170
import logging
171
172
# Enable default logging
173
logging.basicConfig(level=logging.DEBUG)
174
175
# Log all command executions
176
result = sh.ls("-la", _log=True)
177
178
# Custom logger
179
custom_logger = sh.Logger("deployment", {"version": "1.2.3", "env": "production"})
180
181
sh.git("pull", _log=custom_logger)
182
sh.docker("build", "-t", "myapp:latest", ".", _log=custom_logger)
183
sh.docker("push", "myapp:latest", _log=custom_logger)
184
185
# Conditional logging
186
debug_mode = True
187
if debug_mode:
188
logger = sh.Logger("debug")
189
else:
190
logger = None
191
192
sh.make("build", _log=logger)
193
194
# Log analysis
195
class CommandAuditor:
196
def __init__(self):
197
self.commands = []
198
self.logger = sh.Logger("auditor")
199
self.logger.log = self._capture_log
200
201
def _capture_log(self, level, message):
202
self.commands.append({
203
'level': level,
204
'message': message,
205
'timestamp': time.time()
206
})
207
208
def get_summary(self):
209
return {
210
'total_commands': len(self.commands),
211
'errors': len([c for c in self.commands if c['level'] == 'error']),
212
'commands': self.commands
213
}
214
215
auditor = CommandAuditor()
216
sh.ls(_log=auditor.logger)
217
sh.pwd(_log=auditor.logger)
218
print(auditor.get_summary())
219
```
220
221
### Command Introspection
222
223
Tools for inspecting and analyzing commands before or after execution.
224
225
```python { .api }
226
class Command:
227
def __str__(self) -> str:
228
"""String representation showing executable path and baked arguments."""
229
230
def __repr__(self) -> str:
231
"""Formal string representation of the Command object."""
232
233
def __eq__(self, other) -> bool:
234
"""Compare Command objects for equality."""
235
```
236
237
Usage examples:
238
239
```python
240
import sh
241
242
# Command discovery and validation
243
def validate_dependencies(commands):
244
"""Check if required commands are available."""
245
missing = []
246
available = []
247
248
for cmd_name in commands:
249
try:
250
cmd = sh.Command(cmd_name)
251
available.append({
252
'name': cmd_name,
253
'command': str(cmd),
254
'version': get_version(cmd)
255
})
256
except sh.CommandNotFound:
257
missing.append(cmd_name)
258
259
return available, missing
260
261
def get_version(cmd):
262
"""Try to get version information for a command."""
263
version_flags = ['--version', '-V', '-v']
264
265
for flag in version_flags:
266
try:
267
output = cmd(flag)
268
return output.split('\n')[0] # First line usually has version
269
except sh.ErrorReturnCode:
270
continue
271
272
return "unknown"
273
274
# Check deployment dependencies
275
required_tools = ['git', 'docker', 'kubectl', 'helm']
276
available, missing = validate_dependencies(required_tools)
277
278
print("Available tools:")
279
for tool in available:
280
print(f" {tool['name']}: {tool['command']} ({tool['version']})")
281
282
if missing:
283
print(f"Missing tools: {missing}")
284
exit(1)
285
286
# Command introspection
287
git_cmd = sh.git
288
print(f"Git command: {git_cmd}")
289
print(f"Git string representation: {str(git_cmd)}")
290
print(f"Git repr: {repr(git_cmd)}")
291
292
# Command comparison
293
def compare_commands(*cmd_names):
294
"""Compare different commands and their capabilities."""
295
results = {}
296
297
for name in cmd_names:
298
try:
299
cmd = sh.Command(name)
300
cmd_str = str(cmd)
301
results[name] = {
302
'available': True,
303
'command': cmd_str,
304
'repr': repr(cmd)
305
}
306
except sh.CommandNotFound:
307
results[name] = {'available': False}
308
309
return results
310
311
# Compare different versions of python
312
python_variants = compare_commands('python', 'python3', 'python3.9', 'python3.10')
313
for name, info in python_variants.items():
314
if info['available']:
315
print(f"{name}: {info['command']}")
316
else:
317
print(f"{name}: not available")
318
```
319
320
### Advanced Stream Buffering
321
322
Low-level stream buffering implementation for advanced users who need fine-grained control over I/O processing.
323
324
```python { .api }
325
class StreamBufferer:
326
"""
327
Internal buffering implementation for command I/O streams.
328
329
Most users don't need this - it's primarily used internally by sh
330
but available for advanced buffering control scenarios.
331
332
Parameters:
333
- buffer_size: int = 0 (unbuffered), 1 (line-buffered), N (N-byte buffered)
334
- encoding: str = character encoding for text processing
335
"""
336
337
def __init__(self, buffer_size: int = 0, encoding: str = 'utf-8'): ...
338
```
339
340
Usage examples:
341
342
```python
343
import sh
344
345
# Most users should use standard command execution
346
# This is only for advanced scenarios requiring custom buffering control
347
bufferer = sh.StreamBufferer(buffer_size=1024, encoding='utf-8')
348
349
# Note: StreamBufferer is primarily an internal implementation detail
350
# Standard sh command execution handles buffering automatically
351
```
352
353
### Environment and Configuration
354
355
Utilities for managing environment variables and command configuration.
356
357
```python { .api }
358
def __call__(self, *args, _env=None, _cwd=None, **kwargs):
359
"""
360
Execute command with environment customization.
361
362
Parameters:
363
- _env: dict = environment variables to set
364
- _cwd: str = working directory for command
365
366
Returns:
367
str: Command output
368
"""
369
```
370
371
Usage examples:
372
373
```python
374
import sh
375
import os
376
377
# Environment management
378
def with_env(**env_vars):
379
"""Context manager for temporary environment variables."""
380
old_env = {}
381
382
try:
383
# Set new environment variables
384
for key, value in env_vars.items():
385
old_env[key] = os.environ.get(key)
386
os.environ[key] = str(value)
387
yield
388
finally:
389
# Restore old environment
390
for key, old_value in old_env.items():
391
if old_value is None:
392
os.environ.pop(key, None)
393
else:
394
os.environ[key] = old_value
395
396
# Usage
397
with with_env(DEBUG=1, LOG_LEVEL="verbose"):
398
sh.my_app("--config", "production.conf")
399
400
# Per-command environment
401
current_env = os.environ.copy()
402
current_env.update({
403
'PYTHONPATH': '/custom/path',
404
'DEBUG': '1'
405
})
406
407
result = sh.python("script.py", _env=current_env)
408
409
# Configuration helpers
410
class ConfigManager:
411
def __init__(self, config_file="config.json"):
412
self.config = self.load_config(config_file)
413
414
def load_config(self, file_path):
415
"""Load configuration from file."""
416
try:
417
import json
418
with open(file_path) as f:
419
return json.load(f)
420
except FileNotFoundError:
421
return {}
422
423
def get_env_for_service(self, service_name):
424
"""Get environment variables for a specific service."""
425
service_config = self.config.get(service_name, {})
426
env = os.environ.copy()
427
env.update(service_config.get('env', {}))
428
return env
429
430
def run_service_command(self, service_name, command, *args):
431
"""Run a command with service-specific configuration."""
432
env = self.get_env_for_service(service_name)
433
cwd = self.config.get(service_name, {}).get('working_dir')
434
435
return command(*args, _env=env, _cwd=cwd)
436
437
# Usage
438
config = ConfigManager("services.json")
439
result = config.run_service_command("web", sh.npm, "start")
440
441
# Path management utilities
442
def ensure_command_available(cmd_name, install_hint=None):
443
"""Ensure a command is available, provide installation hint if not."""
444
try:
445
cmd = sh.Command(cmd_name)
446
return cmd
447
except sh.CommandNotFound:
448
hint = install_hint or f"Install {cmd_name} to continue"
449
raise sh.CommandNotFound(f"{cmd_name} not found. {hint}")
450
451
# Usage
452
git = ensure_command_available('git', 'Install git: sudo apt-get install git')
453
docker = ensure_command_available('docker', 'Install Docker: https://docs.docker.com/install/')
454
```
455
456
### Environment Variable Access
457
458
Access environment variables directly through the sh module interface using attribute access.
459
460
```python { .api }
461
# Environment variables are accessible as module attributes
462
# sh.ENVIRONMENT_VARIABLE returns the value of $ENVIRONMENT_VARIABLE
463
```
464
465
Usage examples:
466
467
```python
468
import sh
469
import os
470
471
# Access environment variables via sh module
472
home_dir = sh.HOME
473
print(f"Home directory: {home_dir}")
474
475
# Access PATH environment variable
476
path_var = sh.PATH
477
print(f"PATH: {path_var}")
478
479
# Check if environment variable exists
480
try:
481
custom_var = sh.MY_CUSTOM_VAR
482
print(f"Custom variable: {custom_var}")
483
except sh.CommandNotFound:
484
print("MY_CUSTOM_VAR not set")
485
486
# Compare with os.environ access
487
print("Via sh.HOME:", sh.HOME)
488
print("Via os.environ:", os.environ.get('HOME'))
489
490
# Dynamic environment variable access
491
def get_env_var(var_name):
492
"""Get environment variable via sh module."""
493
try:
494
return getattr(sh, var_name)
495
except sh.CommandNotFound:
496
return None
497
498
user = get_env_var('USER')
499
shell = get_env_var('SHELL')
500
editor = get_env_var('EDITOR')
501
502
print(f"User: {user}, Shell: {shell}, Editor: {editor}")
503
```
504
505
### Module Interface and Context Management
506
507
Advanced module interface features including the deprecated _args context manager and module-level baking functionality.
508
509
```python { .api }
510
def _args(**kwargs):
511
"""
512
Deprecated context manager for setting default command arguments.
513
514
Note: This is deprecated. Consider using sh.bake() or Command.bake() instead.
515
516
Parameters:
517
- **kwargs: Default arguments to apply to all commands in context
518
519
Returns:
520
ContextManager: Context manager for temporary default arguments
521
"""
522
523
def bake(**kwargs):
524
"""
525
Create a new sh module instance with baked-in default arguments.
526
527
Parameters:
528
- **kwargs: Default execution options for all commands
529
530
Returns:
531
SelfWrapper: New sh module instance with default arguments
532
"""
533
```
534
535
Usage examples:
536
537
```python
538
import sh
539
540
# Module-level baking (recommended approach)
541
sh_verbose = sh.bake(_out=lambda line: print(f"CMD: {line.strip()}"))
542
543
# All commands through sh_verbose will have verbose output
544
sh_verbose.ls("-la")
545
sh_verbose.pwd()
546
547
# Create sh instance with specific environment
548
production_sh = sh.bake(_env={"ENVIRONMENT": "production"})
549
production_sh.deploy_script()
550
551
# Create sh instance with common options
552
background_sh = sh.bake(_bg=True)
553
proc1 = background_sh.long_running_task()
554
proc2 = background_sh.another_task()
555
556
# Legacy _args usage (deprecated, avoid in new code)
557
with sh._args(_timeout=30):
558
# All commands in this block have 30-second timeout
559
sh.curl("http://slow-server.com")
560
sh.wget("http://large-file.com/download")
561
562
# Preferred modern approach using bake
563
timeout_sh = sh.bake(_timeout=30)
564
timeout_sh.curl("http://slow-server.com")
565
timeout_sh.wget("http://large-file.com/download")
566
```