Source code for pyralph.shell

#!/usr/bin/env python3
"""Shell module for Ralph.

This module contains the Shell class that provides subprocess execution
functionality for the Ralph CLI tool.
"""
import subprocess
from typing import List, Optional, Tuple

from .config import CONF


[docs] class Shell: """Safe wrapper for subprocess calls."""
[docs] @staticmethod def run(command: str, timeout: int = 30) -> Tuple[str, str, int]: """ Execute a shell command and capture its output. Args: command: The shell command to execute timeout: Maximum seconds to wait for command completion Returns: Tuple of (stdout, stderr, return_code) Security Note: This method uses shell=True which enables shell features (pipes, wildcards, variable expansion) but introduces command injection risks if `command` contains unsanitized user input. Safe usage (internal/trusted sources): - Hardcoded commands (e.g., "pytest", "tree -L 2") - Commands from configuration files controlled by the user - Agent-generated commands (trusted AI output) Unsafe usage (AVOID): - Commands built from external/untrusted input - Commands containing unvalidated user data This is acceptable here because: 1. Commands originate from trusted sources (config, agents) 2. The tool runs locally with user's own permissions 3. Shell features (pipes, globs) are required for functionality """ try: result = subprocess.run( command, shell=True, capture_output=True, text=True, encoding='utf-8', timeout=timeout ) return result.stdout, result.stderr, result.returncode except subprocess.TimeoutExpired: return "", "Command Timed Out", 1 except Exception as e: return "", str(e), 1
# Default exclusion patterns for file tree DEFAULT_TREE_IGNORE = ['node_modules', 'venv', '.git', '.ralph', '__pycache__']
[docs] @staticmethod def get_file_tree(depth: int = 2, ignore: Optional[List[str]] = None) -> str: """ Generate a file tree representation of the project directory. Args: depth: Maximum directory depth to traverse (default: 2) ignore: List of directory/file patterns to exclude (default: node_modules, venv, .git, .ralph, __pycache__) Returns: String representation of the directory tree """ if ignore is None: ignore = Shell.DEFAULT_TREE_IGNORE # Build the ignore pattern for tree command ignore_pattern = '|'.join(ignore) if ignore else '' # We explicitly list '.' to ensure we are looking at CWD cmd = f"tree -L {depth} --noreport" if ignore_pattern: cmd += f" -I '{ignore_pattern}'" stdout, _, code = Shell.run(cmd) if code == 0 and stdout.strip(): return stdout # Fallback python walker using CWD ignore_set = set(ignore) if ignore else set() lines = [] for path in CONF.BASE_DIR.glob('*'): if path.name not in ignore_set: lines.append(f"├── {path.name}") return "\n".join(lines)