# Command Line Interface

Complete CLI implementation providing file discovery, configuration loading, batch processing, and various output modes for formatting Python code from the command line.

## Capabilities

### Main CLI Entry Point

The primary command-line interface function with comprehensive option parsing and file processing.

```python { .api }
def main(
    ctx: click.Context,
    code: Optional[str] = None,
    line_length: int = DEFAULT_LINE_LENGTH,
    target_version: list[TargetVersion] = [],
    pyi: bool = False,
    ipynb: bool = False,
    python_cell_magics: Sequence[str] = [],
    skip_source_first_line: bool = False,
    skip_string_normalization: bool = False,
    skip_magic_trailing_comma: bool = False,
    preview: bool = False,
    unstable: bool = False,
    enable_unstable_feature: list[Preview] = [],
    check: bool = False,
    diff: bool = False,
    color: bool = False,
    line_ranges: Sequence[str] = [],
    fast: bool = False,
    required_version: Optional[str] = None,
    exclude: Optional[Pattern[str]] = None,
    extend_exclude: Optional[Pattern[str]] = None,
    force_exclude: Optional[Pattern[str]] = None,
    stdin_filename: Optional[str] = None,
    include: Pattern[str] = DEFAULT_INCLUDES,
    workers: Optional[int] = None,
    quiet: bool = False,
    verbose: bool = False,
    src: tuple[str, ...] = (),
    config: Optional[str] = None
) -> None:
    """
    Main CLI entry point with comprehensive option parsing.

    Parameters:
    - ctx: Click context for CLI handling
    - code: Optional code string to format (instead of files)  
    - line_length: Maximum line length (default 88)
    - target_version: List of Python versions to target
    - pyi: Format as .pyi stub file
    - ipynb: Format as Jupyter notebook
    - python_cell_magics: IPython cell magics to recognize as Python
    - skip_source_first_line: Skip shebang lines
    - skip_string_normalization: Don't normalize string quotes
    - skip_magic_trailing_comma: Don't use trailing comma splitting
    - preview: Enable preview features
    - unstable: Enable unstable features  
    - enable_unstable_feature: List of specific preview features to enable
    - check: Exit with error if reformatting needed
    - diff: Output unified diff instead of rewriting
    - color: Colorize diff output
    - line_ranges: Line ranges to format (e.g., "1-10,20-30")
    - fast: Skip AST safety checks
    - required_version: Require specific Black version
    - exclude: File pattern to exclude (regex)
    - extend_exclude: Additional exclude pattern
    - force_exclude: Force exclude pattern (overrides include)
    - stdin_filename: Filename for stdin input
    - include: File pattern to include (default: Python files)
    - workers: Number of parallel workers
    - quiet: Minimal output
    - verbose: Detailed output
    - src: Source files/directories to format
    - config: Configuration file path

    Note:
    Decorated with comprehensive Click options for all CLI functionality
    """
```

### File Discovery

Functions for discovering and filtering source files to format.

```python { .api }
def get_sources(
    *,
    root: Path,
    src: tuple[str, ...], 
    quiet: bool,
    verbose: bool,
    include: Pattern[str],
    exclude: Optional[Pattern[str]],
    extend_exclude: Optional[Pattern[str]], 
    force_exclude: Optional[Pattern[str]],
    report: Report,
    stdin_filename: Optional[str]
) -> set[Path]:
    """
    Compute the set of files to format.

    Parameters:
    - root: Project root directory
    - src: Source file/directory paths
    - quiet: Suppress output messages
    - verbose: Enable detailed output
    - include: File pattern to include
    - exclude: Base exclude pattern
    - extend_exclude: Additional exclude pattern
    - force_exclude: Force exclude (overrides include)
    - report: Report object for tracking results
    - stdin_filename: Filename when reading from stdin

    Returns:
    Set of Path objects to format

    Note:
    - Automatically discovers .py, .pyi, and .ipynb files
    - Respects .gitignore patterns
    - Handles recursive directory traversal
    """
```

### Configuration Loading

Functions for loading and parsing Black configuration from pyproject.toml files.

```python { .api }
def read_pyproject_toml(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Optional[str]:
    """
    Parse Black configuration from pyproject.toml files.

    Parameters:
    - ctx: Click context
    - param: Click parameter being processed
    - value: Path to configuration file (optional)

    Returns:
    Path to found configuration file, or None

    Note:
    - Searches for pyproject.toml in project hierarchy
    - Loads [tool.black] section configuration
    - Merges with command-line arguments
    """

def validate_regex(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Optional[Pattern[str]]:
    """
    Validate and compile regex patterns for file filtering.

    Parameters:
    - ctx: Click context
    - param: Click parameter being validated
    - value: Regex pattern string

    Returns:
    Compiled regex Pattern object, or None

    Raises:
    - click.BadParameter: If regex is invalid
    """
```

## File Processing Functions

### Project Discovery

Utilities from the `black.files` module for project structure analysis.

```python { .api }
def find_project_root(srcs: tuple[str, ...], stdin_filename: Optional[str] = None) -> tuple[Path, str]:
    """
    Find the project root directory and reason.

    Parameters:
    - srcs: Source file/directory paths
    - stdin_filename: Filename for stdin processing

    Returns:
    Tuple of (project_root_path, reason_string)
    """

def find_pyproject_toml(path_search_start: tuple[str, ...], stdin_filename: Optional[str] = None) -> Optional[str]:
    """
    Find pyproject.toml configuration file.

    Parameters:
    - path_search_start: Paths to start search from  
    - stdin_filename: Filename for stdin processing

    Returns:
    Path to pyproject.toml file, or None if not found
    """

def find_user_pyproject_toml() -> Path:
    """
    Find user-level pyproject.toml configuration.

    Returns:
    Path to user configuration file
    """

def parse_pyproject_toml(path_config: str) -> dict[str, Any]:
    """
    Parse Black configuration from pyproject.toml file.

    Parameters:
    - path_config: Path to configuration file

    Returns:  
    Dictionary of configuration values

    Raises:
    - OSError: If file cannot be read
    - tomllib.TOMLDecodeError: If TOML is invalid
    """
```

### File Generation and Filtering

```python { .api }
def gen_python_files(
    paths: Iterable[Path],
    root: Path,
    include: Pattern[str], 
    exclude: Pattern[str],
    extend_exclude: Optional[Pattern[str]],
    force_exclude: Optional[Pattern[str]],
    report: Report,
    gitignore: Optional[PathSpec] = None
) -> Iterator[Path]:
    """
    Generate Python files from given paths with filtering.

    Parameters:
    - paths: Iterable of source paths
    - root: Project root directory
    - include: Include pattern
    - exclude: Exclude pattern  
    - extend_exclude: Additional exclude pattern
    - force_exclude: Force exclude pattern
    - report: Report object for tracking
    - gitignore: Optional gitignore patterns

    Yields:
    Path objects for files to process
    """

def get_gitignore(root: Path) -> PathSpec:
    """
    Get gitignore patterns for the project.

    Parameters:
    - root: Project root directory

    Returns:
    PathSpec object with gitignore patterns
    """

def path_is_excluded(
    normalized_path: str,
    pattern: Optional[Pattern[str]]
) -> bool:
    """
    Check if path matches exclusion pattern.

    Parameters:
    - normalized_path: Normalized file path
    - pattern: Regex pattern to match against

    Returns:
    True if path should be excluded
    """
```

## Line Range Processing

Functions for parsing and handling line range specifications.

```python { .api }
def parse_line_ranges(line_ranges: Sequence[str]) -> list[tuple[int, int]]:
    """
    Parse line range specifications from CLI arguments.

    Parameters:
    - line_ranges: List of line range strings (e.g., ["1-10", "20-30"])

    Returns:
    List of (start_line, end_line) tuples

    Note:
    - Supports formats: "N", "N-M", "N-", "-M"
    - Line numbers are 1-indexed
    - Overlapping ranges are merged
    
    Raises:
    - ValueError: If range format is invalid
    """

def sanitized_lines(
    src_contents: str,
    lines: Collection[tuple[int, int]]
) -> Collection[tuple[int, int]]:
    """
    Sanitize line ranges against actual file content.

    Parameters:
    - src_contents: Source file content
    - lines: Line ranges to sanitize

    Returns:
    Sanitized line ranges within file bounds
    """

def adjusted_lines(
    original_lines: Collection[tuple[int, int]],
    src_contents: str,
    dst_contents: str
) -> Collection[tuple[int, int]]:
    """
    Adjust line ranges after formatting changes.

    Parameters:
    - original_lines: Original line ranges
    - src_contents: Original file content
    - dst_contents: Formatted file content

    Returns:
    Adjusted line ranges accounting for formatting changes
    """
```

## Usage Examples

### Basic CLI Usage

```python
import subprocess
import sys

# Format single file
result = subprocess.run([
    sys.executable, "-m", "black", "script.py"
], capture_output=True, text=True)

# Format with options
result = subprocess.run([
    sys.executable, "-m", "black", 
    "--line-length", "79",
    "--target-version", "py39",
    "--diff",
    "src/"
], capture_output=True, text=True)

print(result.stdout)
```

### Programmatic CLI Access

```python
import click
import black

# Create Click context and call main
ctx = click.Context(black.main)
try:
    black.main.invoke(ctx, 
        src=('src/',),
        line_length=79,
        target_version=[black.TargetVersion.PY39],
        diff=True,
        check=False
    )
except SystemExit as e:
    print(f"Exit code: {e.code}")
```

### File Discovery

```python
from pathlib import Path
import re

# Setup for file discovery
root = Path(".")
src_paths = ("src/", "tests/")
include = re.compile(r"\.pyi?$")
exclude = re.compile(r"/(\.git|__pycache__|build)/")

report = black.Report()

# Find files to format
sources = black.get_sources(
    root=root,
    src=src_paths,
    quiet=False,
    verbose=True, 
    include=include,
    exclude=exclude,
    extend_exclude=None,
    force_exclude=None,
    report=report,
    stdin_filename=None
)

print(f"Found {len(sources)} files to format:")
for source in sorted(sources):
    print(f"  {source}")
```

### Configuration Loading

```python
# Find and load configuration
config_path = black.find_pyproject_toml((".",), stdin_filename=None)
if config_path:
    config = black.parse_pyproject_toml(config_path)
    print(f"Loaded config from {config_path}:")
    print(config)
    
    # Apply config to mode
    mode_kwargs = {}
    if "line_length" in config:
        mode_kwargs["line_length"] = config["line_length"]
    if "target_version" in config:
        target_versions = {
            black.TargetVersion[v.upper().replace(".", "")]
            for v in config["target_version"]
        }
        mode_kwargs["target_versions"] = target_versions
    
    mode = black.Mode(**mode_kwargs)
```

### Line Range Formatting

```python
# Parse line ranges from CLI-style strings
range_specs = ["1-10", "20-25", "50-"]
line_ranges = black.parse_line_ranges(range_specs)
print(f"Parsed ranges: {line_ranges}")

# Format specific lines only
code = open("example.py").read()
formatted = black.format_str(
    code, 
    mode=black.Mode(),
    lines=line_ranges
)
```

### Batch Processing

```python
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

def format_file(file_path: Path) -> bool:
    """Format a single file and return if changed."""
    try:
        return black.format_file_in_place(
            file_path,
            fast=False,
            mode=black.Mode(),
            write_back=black.WriteBack.YES
        )
    except Exception as e:
        print(f"Error formatting {file_path}: {e}")
        return False

# Process files in parallel
source_files = list(Path("src").glob("**/*.py"))
with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(format_file, source_files))

changed_count = sum(results)
print(f"Formatted {changed_count} files")
```

### Custom Output Handling

```python
import io
import contextlib

# Capture CLI output
output_buffer = io.StringIO()
with contextlib.redirect_stdout(output_buffer):
    try:
        ctx = click.Context(black.main)
        black.main.invoke(ctx,
            src=('example.py',),
            diff=True,
            color=False
        )
    except SystemExit:
        pass

diff_output = output_buffer.getvalue()
print("Captured diff:")
print(diff_output)
```

## Types

```python { .api }
# Path processing types
PathLike = Union[str, Path]
PathSpec = pathspec.PathSpec
Pattern = re.Pattern[str]

# File processing
FileList = set[Path]
SourcePaths = tuple[str, ...]

# Configuration
ConfigDict = dict[str, Any]
LineRange = tuple[int, int]
LineRanges = Collection[LineRange]
```