Source code for atloop.tools.filesystem.glob_files

"""Glob tool for file matching with gitignore-style patterns."""

from typing import Any, Dict, Optional

from atloop.runtime.sandbox_adapter import SandboxAdapter
from atloop.tools.base import BaseTool, ToolResult


[docs] class GlobFilesTool(BaseTool): """ Tool for finding files using gitignore-style glob patterns. **Features:** - Supports common glob patterns (*, **) - Recursive directory searching - File filtering by extension or pattern **Use cases:** - Finding all Python files: `*.py` - Finding all test files: `test_*.py` - Finding files recursively: `**/*.js` - Listing files in a directory: `dir/*.txt` """
[docs] def __init__(self, sandbox: SandboxAdapter): """ Initialize glob files tool. Args: sandbox: Sandbox adapter instance """ self.sandbox = sandbox
@property def name(self) -> str: """Tool name.""" return "glob" @property def description(self) -> str: """Tool description.""" return "文件匹配工具(支持 Gitignore 样式的 glob 模式)"
[docs] def validate_args(self, args: Dict[str, Any]) -> tuple[bool, Optional[str]]: """Validate arguments.""" if "pattern" not in args: return False, "Missing required argument: 'pattern'" if not isinstance(args["pattern"], str): return False, "Argument 'pattern' must be a string" if "max_results" in args and not isinstance(args.get("max_results"), int): return False, "Argument 'max_results' must be an integer" return True, None
[docs] def execute(self, args: Dict[str, Any]) -> ToolResult: """ Execute glob tool to find files matching a pattern. **Args:** args: Tool arguments dictionary - pattern (str, required): Glob pattern to match files. Supports: - `*.py`: Matches all .py files in current directory - `**/*.py`: Matches all .py files recursively - `test_*.py`: Matches files starting with "test_" - `dir/*.txt`: Matches .txt files in "dir" directory - max_results (int, optional): Maximum number of results to return. Default: 100. Use this to limit output for very large matches. **Returns:** ToolResult with: - ok (bool): True if pattern was executed successfully (no errors in stderr) - stdout (str): Formatted list of matched files, one per line - stderr (str): Error messages if any (empty string means success) - meta (dict): Contains pattern, max_results, matched_count, matched_files **Examples:** # Find all Python files glob(pattern="*.py") # Find all Python files recursively glob(pattern="**/*.py") # Find test files glob(pattern="test_*.py") # Find files in specific directory glob(pattern="src/*.py") # Limit results glob(pattern="*.py", max_results=10) **Pattern Support:** - `*`: Matches any characters (except path separator) - `**`: Matches any characters including path separators (recursive) - Directory patterns: `dir/*.ext` matches files in "dir" directory - Leading `./` is optional and ignored **Error Handling:** - Success is determined by stderr content, not exit code - If stderr is empty, the operation succeeded - Empty results (no matches) is considered success (ok=True) - Check stderr for specific error messages if ok=False """ pattern = args["pattern"] max_results = args.get("max_results", 100) # Use find command with glob pattern matching # Support common glob patterns: # - *.py: matches all .py files # - **/*.py: matches all .py files recursively # - test_*.py: matches files starting with test_ # - {*.py,*.js}: multiple patterns (not directly supported, but can be handled) # Convert glob pattern to find command # For simple patterns, use find with -name # For ** patterns, use find recursively if "**" in pattern: # Recursive pattern: **/*.py -> find . -name "*.py" # Remove **/ prefix if present name_pattern = pattern.replace("**/", "").replace("./", "") cmd = f"find . -type f -name {self._quote_pattern(name_pattern)} 2>/dev/null | head -n {max_results}" elif pattern.startswith("./"): # Pattern like ./dir/*.py pattern_clean = pattern[2:] # Remove ./ if "/" in pattern_clean: # Has directory: dir/*.py parts = pattern_clean.split("/", 1) dir_part = parts[0] name_pattern = parts[1] cmd = f"find ./{dir_part} -maxdepth 1 -type f -name {self._quote_pattern(name_pattern)} 2>/dev/null | head -n {max_results}" else: # Just filename pattern: *.py cmd = f"find . -maxdepth 1 -type f -name {self._quote_pattern(pattern_clean)} 2>/dev/null | head -n {max_results}" else: # Simple pattern: *.py, test_*.py # Check if pattern contains directory separator if "/" in pattern: # Pattern like dir/*.py parts = pattern.split("/", 1) dir_part = parts[0] name_pattern = parts[1] cmd = f"find ./{dir_part} -maxdepth 1 -type f -name {self._quote_pattern(name_pattern)} 2>/dev/null | head -n {max_results}" else: # Just filename pattern cmd = f"find . -maxdepth 1 -type f -name {self._quote_pattern(pattern)} 2>/dev/null | head -n {max_results}" result = self._run_command(cmd, timeout_sec=30) # Check for errors in stderr, not exit code stderr = result.get("stderr", "") if stderr.strip(): return ToolResult( ok=False, stdout="", stderr=stderr or "Failed to execute glob pattern", meta={"pattern": pattern, "max_results": max_results}, ) # Parse results - each line is a file path stdout = result.get("stdout", "") matched_files = [line.strip() for line in stdout.split("\n") if line.strip()] # Remove leading ./ if present matched_files = [f.lstrip("./") for f in matched_files] # Format output if matched_files: output = f"Found {len(matched_files)} file(s) matching pattern '{pattern}':\n" for file_path in matched_files: output += f" - {file_path}\n" else: output = f"No files found matching pattern '{pattern}'" return ToolResult( ok=True, stdout=output, stderr=stderr, meta={ "pattern": pattern, "max_results": max_results, "matched_count": len(matched_files), "matched_files": matched_files, }, )
def _quote_pattern(self, pattern: str) -> str: """Quote pattern for shell command.""" import shlex return shlex.quote(pattern) def _run_command(self, cmd: str, timeout_sec: int = 600) -> Dict[str, Any]: """Run a shell command in sandbox.""" return self.sandbox.exec_shell( command=cmd, workdir="/workspace", timeout_seconds=timeout_sec, )