0
# Utilities and Helper Functions
1
2
Utility functions for file handling, stream management, text formatting, and other common CLI application needs. Click provides a comprehensive set of helper functions that simplify common command-line application tasks.
3
4
## Capabilities
5
6
### File and Stream Operations
7
8
Functions for handling files, streams, and I/O operations with enhanced features.
9
10
```python { .api }
11
def open_file(filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False):
12
"""
13
Open file with special handling for "-" (stdin/stdout).
14
15
Parameters:
16
- filename: File path or "-" for stdin/stdout
17
- mode: File open mode ('r', 'w', 'a', 'rb', 'wb', etc.)
18
- encoding: Text encoding (None for binary modes)
19
- errors: Error handling strategy ('strict', 'ignore', 'replace')
20
- lazy: Open file lazily on first access
21
- atomic: Write to temporary file then move (for write modes)
22
23
Returns:
24
File object or LazyFile/KeepOpenFile wrapper
25
"""
26
27
def get_binary_stream(name):
28
"""
29
Get system binary stream.
30
31
Parameters:
32
- name: Stream name ("stdin", "stdout", "stderr")
33
34
Returns:
35
Binary stream object
36
"""
37
38
def get_text_stream(name, encoding=None, errors="strict"):
39
"""
40
Get system text stream.
41
42
Parameters:
43
- name: Stream name ("stdin", "stdout", "stderr")
44
- encoding: Text encoding (None for system default)
45
- errors: Error handling strategy
46
47
Returns:
48
Text stream object
49
"""
50
```
51
52
**Usage Examples:**
53
54
```python
55
@click.command()
56
@click.option('--input', default='-', help='Input file (- for stdin)')
57
@click.option('--output', default='-', help='Output file (- for stdout)')
58
def process_streams(input, output):
59
"""Process data from input to output."""
60
with click.open_file(input, 'r') as infile:
61
with click.open_file(output, 'w') as outfile:
62
for line in infile:
63
processed = line.upper()
64
outfile.write(processed)
65
66
@click.command()
67
@click.option('--output', type=click.Path())
68
def atomic_write(output):
69
"""Write file atomically."""
70
with click.open_file(output, 'w', atomic=True) as f:
71
f.write('This will be written atomically\n')
72
f.write('Either all content is written or none\n')
73
click.echo(f'File written atomically to {output}')
74
75
@click.command()
76
def stream_examples():
77
"""Demonstrate stream operations."""
78
# Get system streams
79
stdout = click.get_text_stream('stdout')
80
stderr = click.get_text_stream('stderr')
81
82
stdout.write('This goes to stdout\n')
83
stderr.write('This goes to stderr\n')
84
85
# Binary streams
86
binary_stdout = click.get_binary_stream('stdout')
87
binary_stdout.write(b'Binary data to stdout\n')
88
```
89
90
### Directory and Path Utilities
91
92
Functions for handling application directories and path operations.
93
94
```python { .api }
95
def get_app_dir(app_name, roaming=True, force_posix=False):
96
"""
97
Get application configuration directory.
98
99
Parameters:
100
- app_name: Name of the application
101
- roaming: Use roaming directory on Windows
102
- force_posix: Force POSIX-style paths even on Windows
103
104
Returns:
105
Path to application directory
106
107
Platform behavior:
108
- Windows: %APPDATA%\\app_name (roaming) or %LOCALAPPDATA%\\app_name
109
- macOS: ~/Library/Application Support/app_name
110
- Linux: ~/.config/app_name (or $XDG_CONFIG_HOME/app_name)
111
"""
112
113
def format_filename(filename, shorten=False):
114
"""
115
Format filename for display.
116
117
Parameters:
118
- filename: Filename to format
119
- shorten: Shorten very long filenames
120
121
Returns:
122
Formatted filename string
123
"""
124
```
125
126
**Usage Examples:**
127
128
```python
129
import os
130
131
@click.command()
132
@click.argument('app_name')
133
def show_app_dir(app_name):
134
"""Show application directory for given app name."""
135
app_dir = click.get_app_dir(app_name)
136
click.echo(f'App directory: {app_dir}')
137
138
# Create directory if it doesn't exist
139
os.makedirs(app_dir, exist_ok=True)
140
141
# Create a config file
142
config_file = os.path.join(app_dir, 'config.ini')
143
with open(config_file, 'w') as f:
144
f.write(f'[{app_name}]\n')
145
f.write('debug = false\n')
146
147
click.echo(f'Created config: {click.format_filename(config_file)}')
148
149
@click.command()
150
def config_example():
151
"""Example of using app directory for configuration."""
152
app_dir = click.get_app_dir('myapp')
153
config_path = os.path.join(app_dir, 'settings.json')
154
155
# Ensure directory exists
156
os.makedirs(app_dir, exist_ok=True)
157
158
if os.path.exists(config_path):
159
click.echo(f'Loading config from {click.format_filename(config_path)}')
160
else:
161
click.echo(f'Creating default config at {click.format_filename(config_path)}')
162
import json
163
default_config = {
164
'debug': False,
165
'log_level': 'INFO',
166
'max_retries': 3
167
}
168
with open(config_path, 'w') as f:
169
json.dump(default_config, f, indent=2)
170
```
171
172
### Context Management
173
174
Functions for accessing and managing Click contexts.
175
176
```python { .api }
177
def get_current_context(silent=False):
178
"""
179
Get current Click context from thread-local storage.
180
181
Parameters:
182
- silent: Return None instead of raising error if no context
183
184
Returns:
185
Current Context object or None
186
187
Raises:
188
RuntimeError: If no context is available and silent=False
189
"""
190
```
191
192
**Usage Examples:**
193
194
```python
195
def helper_function():
196
"""Helper function that needs access to current context."""
197
ctx = click.get_current_context(silent=True)
198
if ctx:
199
return ctx.obj.get('debug', False)
200
return False
201
202
@click.group()
203
@click.option('--debug', is_flag=True)
204
@click.pass_context
205
def main_cli(ctx, debug):
206
"""Main CLI with shared context."""
207
ctx.ensure_object(dict)
208
ctx.obj['debug'] = debug
209
210
@main_cli.command()
211
def subcommand():
212
"""Subcommand that uses helper function."""
213
debug_mode = helper_function()
214
click.echo(f'Debug mode: {debug_mode}')
215
216
# Context-aware logging
217
def log_message(message, level='INFO'):
218
"""Log message with context-aware formatting."""
219
ctx = click.get_current_context(silent=True)
220
prefix = ''
221
222
if ctx and ctx.obj and ctx.obj.get('verbose'):
223
prefix = f'[{level}] {ctx.info_name}: '
224
225
click.echo(f'{prefix}{message}')
226
227
@main_cli.command()
228
@click.option('--verbose', is_flag=True)
229
@click.pass_context
230
def logging_example(ctx, verbose):
231
"""Example of context-aware logging."""
232
ctx.obj['verbose'] = verbose
233
234
log_message('Starting operation')
235
log_message('Processing data', 'DEBUG')
236
log_message('Operation complete')
237
```
238
239
### Text and String Utilities
240
241
Helper functions for text processing and string manipulation.
242
243
```python { .api }
244
# Internal utility functions (not directly exported but used by Click)
245
def make_str(value):
246
"""Convert value to valid string representation."""
247
248
def make_default_short_help(help, max_length=45):
249
"""Create condensed help string from longer help text."""
250
251
def safecall(func):
252
"""Wrap function to swallow exceptions silently."""
253
```
254
255
**Usage Examples:**
256
257
```python
258
# Custom help formatting
259
def create_short_help(long_help, max_len=40):
260
"""Create short help from long help text."""
261
if not long_help:
262
return None
263
264
# Simple truncation with ellipsis
265
if len(long_help) <= max_len:
266
return long_help
267
268
# Find last space before max length
269
truncated = long_help[:max_len]
270
last_space = truncated.rfind(' ')
271
272
if last_space > max_len * 0.7: # If space is reasonably close
273
return truncated[:last_space] + '...'
274
else:
275
return truncated + '...'
276
277
@click.group()
278
def cli_with_short_help():
279
"""Main CLI group."""
280
pass
281
282
@cli_with_short_help.command()
283
def long_command():
284
"""This is a very long help text that describes in great detail what this command does and provides comprehensive information about its purpose and usage patterns."""
285
click.echo('Command executed')
286
287
# Set short help manually
288
long_command.short_help = create_short_help(long_command.help)
289
```
290
291
### File Wrapper Classes
292
293
Special file classes for advanced file handling scenarios.
294
295
```python { .api }
296
class LazyFile:
297
"""
298
File wrapper that opens the file lazily on first access.
299
Used internally by click.open_file() with lazy=True.
300
"""
301
302
class KeepOpenFile:
303
"""
304
File wrapper that prevents closing when used as context manager.
305
Useful for stdin/stdout that shouldn't be closed.
306
"""
307
308
class PacifyFlushWrapper:
309
"""
310
Wrapper that suppresses BrokenPipeError on flush().
311
Handles cases where output pipe is closed by receiver.
312
"""
313
```
314
315
**Usage Examples:**
316
317
```python
318
# Custom file handling with error recovery
319
@click.command()
320
@click.option('--input', type=click.File('r'))
321
@click.option('--output', type=click.File('w'))
322
def robust_copy(input, output):
323
"""Copy with error handling for broken pipes."""
324
try:
325
for line in input:
326
output.write(line.upper())
327
output.flush() # May raise BrokenPipeError
328
except BrokenPipeError:
329
# Handle gracefully - receiver closed pipe
330
click.echo('Output pipe closed by receiver', err=True)
331
except KeyboardInterrupt:
332
click.echo('\nOperation cancelled', err=True)
333
raise click.Abort()
334
335
# Working with lazy files
336
@click.command()
337
@click.option('--config', type=click.Path())
338
def lazy_config(config):
339
"""Example of lazy file loading."""
340
if config:
341
# File won't be opened until first access
342
with click.open_file(config, 'r', lazy=True) as f:
343
if some_condition():
344
# File is opened here on first read
345
content = f.read()
346
click.echo(f'Config loaded: {len(content)} bytes')
347
# If condition was false, file never gets opened
348
349
# Atomic file operations
350
@click.command()
351
@click.argument('output_file')
352
def atomic_json_write(output_file):
353
"""Write JSON atomically to prevent corruption."""
354
import json
355
import time
356
357
data = {
358
'timestamp': time.time(),
359
'status': 'processing',
360
'items': list(range(1000))
361
}
362
363
# Atomic write - either succeeds completely or fails completely
364
with click.open_file(output_file, 'w', atomic=True) as f:
365
json.dump(data, f, indent=2)
366
# If any error occurs here, original file is unchanged
367
368
click.echo(f'Data written atomically to {output_file}')
369
```
370
371
### Text Formatting and Help Generation
372
373
Functions and classes for formatting help text and wrapping text content.
374
375
```python { .api }
376
class HelpFormatter:
377
def __init__(self, indent_increment=2, width=None, max_width=None):
378
"""
379
Format text-based help pages for commands.
380
381
Parameters:
382
- indent_increment: Additional increment for each indentation level
383
- width: Text width (defaults to terminal width, max 78)
384
- max_width: Maximum width allowed
385
"""
386
387
def write(self, string):
388
"""Write a string into the internal buffer."""
389
390
def indent(self):
391
"""Increase the indentation level."""
392
393
def dedent(self):
394
"""Decrease the indentation level."""
395
396
def write_usage(self, prog, args="", prefix=None):
397
"""
398
Write a usage line into the buffer.
399
400
Parameters:
401
- prog: Program name
402
- args: Whitespace separated list of arguments
403
- prefix: Prefix for the first line (defaults to "Usage: ")
404
"""
405
406
def write_heading(self, heading):
407
"""Write a heading into the buffer."""
408
409
def write_paragraph(self):
410
"""Write a paragraph break into the buffer."""
411
412
def write_text(self, text):
413
"""Write re-indented text with paragraph preservation."""
414
415
def write_dl(self, rows, col_max=30, col_spacing=2):
416
"""
417
Write a definition list (used for options and commands).
418
419
Parameters:
420
- rows: List of (term, description) tuples
421
- col_max: Maximum width of first column
422
- col_spacing: Spaces between columns
423
"""
424
425
def section(self, name):
426
"""Context manager that writes heading and indents."""
427
428
def indentation(self):
429
"""Context manager that increases indentation."""
430
431
def getvalue(self):
432
"""Return the buffer contents as string."""
433
434
def wrap_text(text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False):
435
"""
436
Intelligent text wrapping with paragraph handling.
437
438
Parameters:
439
- text: Text to wrap
440
- width: Maximum line width
441
- initial_indent: Indent for first line
442
- subsequent_indent: Indent for continuation lines
443
- preserve_paragraphs: Handle paragraphs intelligently (separated by empty lines)
444
445
Returns:
446
Wrapped text string
447
448
Notes:
449
- When preserve_paragraphs=True, paragraphs are defined by two empty lines
450
- Lines starting with \\b character are not rewrapped
451
- Handles mixed indentation levels within paragraphs
452
"""
453
```
454
455
**Usage Examples:**
456
457
```python
458
@click.command()
459
def formatting_examples():
460
"""Demonstrate text formatting utilities."""
461
# Create a help formatter
462
formatter = click.HelpFormatter(indent_increment=4, width=60)
463
464
# Write structured help content
465
formatter.write_heading('Options')
466
formatter.indent()
467
formatter.write_dl([
468
('--verbose, -v', 'Enable verbose output'),
469
('--config FILE', 'Configuration file path'),
470
('--help', 'Show this message and exit')
471
])
472
formatter.dedent()
473
474
# Get formatted result
475
help_text = formatter.getvalue()
476
click.echo(help_text)
477
478
# Text wrapping examples
479
long_text = "This is a very long line of text that needs to be wrapped to fit within a reasonable line length for display in terminal applications."
480
481
wrapped = click.wrap_text(long_text, width=40)
482
click.echo("Basic wrapping:")
483
click.echo(wrapped)
484
485
# With indentation
486
indented = click.wrap_text(long_text, width=40,
487
initial_indent=" * ",
488
subsequent_indent=" ")
489
click.echo("\nWith indentation:")
490
click.echo(indented)
491
492
# Paragraph preservation
493
paragraphs = """First paragraph with some text.
494
495
Second paragraph after empty line.
496
497
Indented paragraph that should
498
maintain its indentation level."""
499
500
preserved = click.wrap_text(paragraphs, width=30, preserve_paragraphs=True)
501
click.echo("\nParagraph preservation:")
502
click.echo(preserved)
503
504
# Custom help formatter
505
class CustomHelpFormatter(click.HelpFormatter):
506
"""Custom help formatter with different styling."""
507
508
def write_heading(self, heading):
509
"""Write heading with custom styling."""
510
self.write(f"{'':>{self.current_indent}}=== {heading.upper()} ===\n")
511
512
@click.command()
513
def custom_help_example():
514
"""Command with custom help formatting."""
515
pass
516
517
# Override the formatter class
518
custom_help_example.context_settings = {'formatter_class': CustomHelpFormatter}
519
```
520
521
### Integration Utilities
522
523
Functions for integrating Click with other systems and frameworks.
524
525
```python
526
# Environment integration
527
@click.command()
528
@click.option('--config-dir',
529
envvar='MY_APP_CONFIG_DIR',
530
default=lambda: click.get_app_dir('myapp'),
531
help='Configuration directory')
532
def env_integration(config_dir):
533
"""Example of environment variable integration."""
534
click.echo(f'Using config directory: {config_dir}')
535
536
# Shell integration
537
@click.command()
538
@click.option('--shell-complete', is_flag=True, hidden=True)
539
def shell_integration(shell_complete):
540
"""Command with shell completion support."""
541
if shell_complete:
542
# This would be handled by Click's completion system
543
return
544
545
click.echo('Command executed')
546
547
# Testing utilities integration
548
def create_test_context(command, args=None):
549
"""Create context for testing Click commands."""
550
if args is None:
551
args = []
552
553
return command.make_context('test', args)
554
555
# Example test helper
556
def test_command_helper():
557
"""Helper for testing Click commands."""
558
@click.command()
559
@click.argument('name')
560
@click.option('--greeting', default='Hello')
561
def greet(name, greeting):
562
click.echo(f'{greeting}, {name}!')
563
564
# Create test context
565
ctx = create_test_context(greet, ['World', '--greeting', 'Hi'])
566
567
# This would be used in actual testing
568
return ctx
569
```