luckyd-code 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luckyd_code/__init__.py +54 -0
- luckyd_code/__main__.py +5 -0
- luckyd_code/_agent_loop.py +551 -0
- luckyd_code/_data_dir.py +73 -0
- luckyd_code/agent.py +38 -0
- luckyd_code/analytics/__init__.py +18 -0
- luckyd_code/analytics/reporter.py +195 -0
- luckyd_code/analytics/scanner.py +443 -0
- luckyd_code/analytics/smells.py +316 -0
- luckyd_code/analytics/trends.py +303 -0
- luckyd_code/api.py +473 -0
- luckyd_code/audit_daemon.py +845 -0
- luckyd_code/autonomous_fixer.py +473 -0
- luckyd_code/background.py +159 -0
- luckyd_code/backup.py +237 -0
- luckyd_code/brain/__init__.py +84 -0
- luckyd_code/brain/assembler.py +100 -0
- luckyd_code/brain/chunker.py +345 -0
- luckyd_code/brain/constants.py +73 -0
- luckyd_code/brain/embedder.py +163 -0
- luckyd_code/brain/graph.py +311 -0
- luckyd_code/brain/indexer.py +316 -0
- luckyd_code/brain/parser.py +140 -0
- luckyd_code/brain/retriever.py +234 -0
- luckyd_code/cli.py +894 -0
- luckyd_code/cli_commands/__init__.py +1 -0
- luckyd_code/cli_commands/audit.py +120 -0
- luckyd_code/cli_commands/background.py +83 -0
- luckyd_code/cli_commands/brain.py +87 -0
- luckyd_code/cli_commands/config.py +75 -0
- luckyd_code/cli_commands/dispatcher.py +695 -0
- luckyd_code/cli_commands/sessions.py +41 -0
- luckyd_code/cli_entry.py +147 -0
- luckyd_code/cli_utils.py +112 -0
- luckyd_code/config.py +205 -0
- luckyd_code/context.py +214 -0
- luckyd_code/cost_tracker.py +209 -0
- luckyd_code/error_reporter.py +508 -0
- luckyd_code/exceptions.py +39 -0
- luckyd_code/export.py +126 -0
- luckyd_code/feedback_analyzer.py +290 -0
- luckyd_code/file_watcher.py +258 -0
- luckyd_code/git/__init__.py +11 -0
- luckyd_code/git/auto_commit.py +157 -0
- luckyd_code/git/tools.py +85 -0
- luckyd_code/hooks.py +236 -0
- luckyd_code/indexer.py +280 -0
- luckyd_code/init.py +39 -0
- luckyd_code/keybindings.py +77 -0
- luckyd_code/log.py +55 -0
- luckyd_code/mcp/__init__.py +6 -0
- luckyd_code/mcp/client.py +184 -0
- luckyd_code/memory/__init__.py +19 -0
- luckyd_code/memory/manager.py +339 -0
- luckyd_code/metrics/__init__.py +5 -0
- luckyd_code/model_registry.py +131 -0
- luckyd_code/orchestrator.py +204 -0
- luckyd_code/permissions/__init__.py +1 -0
- luckyd_code/permissions/manager.py +103 -0
- luckyd_code/planner.py +361 -0
- luckyd_code/plugins.py +91 -0
- luckyd_code/py.typed +0 -0
- luckyd_code/retry.py +57 -0
- luckyd_code/router.py +417 -0
- luckyd_code/sandbox.py +156 -0
- luckyd_code/self_critique.py +2 -0
- luckyd_code/self_improve.py +274 -0
- luckyd_code/sessions.py +114 -0
- luckyd_code/settings.py +72 -0
- luckyd_code/skills/__init__.py +8 -0
- luckyd_code/skills/review.py +22 -0
- luckyd_code/skills/security.py +17 -0
- luckyd_code/tasks/__init__.py +1 -0
- luckyd_code/tasks/manager.py +102 -0
- luckyd_code/templates/icon-192.png +0 -0
- luckyd_code/templates/icon-512.png +0 -0
- luckyd_code/templates/index.html +1965 -0
- luckyd_code/templates/manifest.json +14 -0
- luckyd_code/templates/src/app.js +694 -0
- luckyd_code/templates/src/body.html +767 -0
- luckyd_code/templates/src/cdn.txt +2 -0
- luckyd_code/templates/src/style.css +474 -0
- luckyd_code/templates/sw.js +31 -0
- luckyd_code/templates/test.html +6 -0
- luckyd_code/themes.py +48 -0
- luckyd_code/tools/__init__.py +97 -0
- luckyd_code/tools/agent_tools.py +65 -0
- luckyd_code/tools/bash.py +360 -0
- luckyd_code/tools/brain_tools.py +137 -0
- luckyd_code/tools/browser.py +369 -0
- luckyd_code/tools/datetime_tool.py +34 -0
- luckyd_code/tools/dockerfile_gen.py +212 -0
- luckyd_code/tools/file_ops.py +381 -0
- luckyd_code/tools/game_gen.py +360 -0
- luckyd_code/tools/git_tools.py +130 -0
- luckyd_code/tools/git_worktree.py +63 -0
- luckyd_code/tools/path_validate.py +64 -0
- luckyd_code/tools/project_gen.py +187 -0
- luckyd_code/tools/readme_gen.py +227 -0
- luckyd_code/tools/registry.py +157 -0
- luckyd_code/tools/shell_detect.py +109 -0
- luckyd_code/tools/web.py +89 -0
- luckyd_code/tools/youtube.py +187 -0
- luckyd_code/tools_bridge.py +144 -0
- luckyd_code/undo.py +126 -0
- luckyd_code/update.py +60 -0
- luckyd_code/verify.py +360 -0
- luckyd_code/web_app.py +176 -0
- luckyd_code/web_routes/__init__.py +23 -0
- luckyd_code/web_routes/background.py +73 -0
- luckyd_code/web_routes/brain.py +109 -0
- luckyd_code/web_routes/cost.py +12 -0
- luckyd_code/web_routes/files.py +133 -0
- luckyd_code/web_routes/memories.py +94 -0
- luckyd_code/web_routes/misc.py +67 -0
- luckyd_code/web_routes/project.py +48 -0
- luckyd_code/web_routes/review.py +20 -0
- luckyd_code/web_routes/sessions.py +44 -0
- luckyd_code/web_routes/settings.py +43 -0
- luckyd_code/web_routes/static.py +70 -0
- luckyd_code/web_routes/update.py +19 -0
- luckyd_code/web_routes/ws.py +237 -0
- luckyd_code-1.2.2.dist-info/METADATA +297 -0
- luckyd_code-1.2.2.dist-info/RECORD +127 -0
- luckyd_code-1.2.2.dist-info/WHEEL +4 -0
- luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
- luckyd_code-1.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""File operation tools with path traversal protection."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .registry import Tool
|
|
9
|
+
from .path_validate import validate_file_path
|
|
10
|
+
from ..undo import push as undo_push
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger("luckyd_code.tools.file_ops")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _unified_diff(original: str, updated: str, filename: str) -> str:
|
|
16
|
+
"""Return a unified diff string between two texts, or empty if identical."""
|
|
17
|
+
a = original.splitlines(keepends=True)
|
|
18
|
+
b = updated.splitlines(keepends=True)
|
|
19
|
+
diff = list(difflib.unified_diff(a, b, fromfile=f"a/{filename}", tofile=f"b/{filename}"))
|
|
20
|
+
return "".join(diff)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReadTool(Tool):
|
|
24
|
+
name = "Read"
|
|
25
|
+
description = "Read the contents of a file. Supports line offsets and limits."
|
|
26
|
+
parameters = {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"file_path": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Absolute path to the file to read",
|
|
32
|
+
},
|
|
33
|
+
"offset": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"description": "Line number to start reading from (0-indexed)",
|
|
36
|
+
},
|
|
37
|
+
"limit": {
|
|
38
|
+
"type": "integer",
|
|
39
|
+
"description": "Maximum number of lines to read",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"required": ["file_path"],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def run(self, file_path: str, offset: int = 0, limit: int | None = None) -> str: # type: ignore[override]
|
|
46
|
+
try:
|
|
47
|
+
path = validate_file_path(file_path, must_exist=True)
|
|
48
|
+
except (ValueError, FileNotFoundError) as e:
|
|
49
|
+
return f"Error: {e}"
|
|
50
|
+
|
|
51
|
+
if not path.is_file():
|
|
52
|
+
return f"Error: not a file: {file_path}"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines(keepends=True)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return f"Error reading file: {e}"
|
|
58
|
+
|
|
59
|
+
if offset >= len(lines):
|
|
60
|
+
return f"Error: offset {offset} is beyond file length {len(lines)}"
|
|
61
|
+
|
|
62
|
+
selected = lines[offset:]
|
|
63
|
+
if limit is not None:
|
|
64
|
+
selected = selected[:limit]
|
|
65
|
+
|
|
66
|
+
result = "".join(selected)
|
|
67
|
+
total = len(lines)
|
|
68
|
+
start = offset
|
|
69
|
+
end = offset + len(selected) - 1
|
|
70
|
+
|
|
71
|
+
header = f"{path.name} ({total} lines, showing {start}-{end})"
|
|
72
|
+
sep = "-" * len(header)
|
|
73
|
+
return f"{header}\n{sep}\n{result}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class WriteTool(Tool):
|
|
77
|
+
name = "Write"
|
|
78
|
+
description = (
|
|
79
|
+
"Create a new file or overwrite an existing file with new content. "
|
|
80
|
+
"Pass dry_run=true to preview a unified diff of the changes without writing."
|
|
81
|
+
)
|
|
82
|
+
permission_risk = "medium"
|
|
83
|
+
parameters = {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"file_path": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Absolute path to the file to write",
|
|
89
|
+
},
|
|
90
|
+
"content": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Content to write to the file",
|
|
93
|
+
},
|
|
94
|
+
"dry_run": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": (
|
|
97
|
+
"If true, return a unified diff of the proposed change "
|
|
98
|
+
"without modifying the file. Defaults to false."
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"required": ["file_path", "content"],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def run(self, file_path: str, content: str, dry_run: bool = False) -> str: # type: ignore[override]
|
|
106
|
+
try:
|
|
107
|
+
path = validate_file_path(file_path)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
return f"Error: {e}"
|
|
110
|
+
|
|
111
|
+
# Prevent writing files larger than 10MB
|
|
112
|
+
if len(content) > 10 * 1024 * 1024:
|
|
113
|
+
return "Error: content exceeds maximum file size (10MB)"
|
|
114
|
+
|
|
115
|
+
original = path.read_text(encoding="utf-8") if path.exists() else None
|
|
116
|
+
|
|
117
|
+
# Dry-run: show diff without touching the file
|
|
118
|
+
if dry_run:
|
|
119
|
+
if original is None:
|
|
120
|
+
line_count = content.count("\n") + 1
|
|
121
|
+
return f"[dry-run] Would create new file: {file_path} ({line_count} lines)"
|
|
122
|
+
diff = _unified_diff(original, content, path.name)
|
|
123
|
+
if not diff:
|
|
124
|
+
return f"[dry-run] No changes — content is identical to {file_path}"
|
|
125
|
+
return f"[dry-run] Proposed changes to {file_path}:\n\n{diff}"
|
|
126
|
+
|
|
127
|
+
# Save undo info before writing
|
|
128
|
+
undo_push(file_path, original or "", "Write")
|
|
129
|
+
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
try:
|
|
132
|
+
path.write_text(content, encoding="utf-8")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return f"Error writing file: {e}"
|
|
135
|
+
|
|
136
|
+
# Show a concise summary of what changed
|
|
137
|
+
if original is not None:
|
|
138
|
+
diff = _unified_diff(original, content, path.name)
|
|
139
|
+
changed_lines = sum(
|
|
140
|
+
1 for line in diff.splitlines()
|
|
141
|
+
if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))
|
|
142
|
+
)
|
|
143
|
+
return (
|
|
144
|
+
f"Successfully wrote {len(content)} bytes to {file_path} "
|
|
145
|
+
f"({changed_lines} lines changed)"
|
|
146
|
+
)
|
|
147
|
+
return f"Successfully wrote {len(content)} bytes to {file_path} (new file)"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class EditTool(Tool):
|
|
151
|
+
name = "Edit"
|
|
152
|
+
description = "Edit an existing file by replacing text. Performs an exact string replacement."
|
|
153
|
+
permission_risk = "medium"
|
|
154
|
+
parameters = {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"file_path": {
|
|
158
|
+
"type": "string",
|
|
159
|
+
"description": "Absolute path to the file to edit",
|
|
160
|
+
},
|
|
161
|
+
"old_string": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "Text to replace (must be unique in the file)",
|
|
164
|
+
},
|
|
165
|
+
"new_string": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Text to replace it with",
|
|
168
|
+
},
|
|
169
|
+
"replace_all": {
|
|
170
|
+
"type": "boolean",
|
|
171
|
+
"description": "Replace all occurrences instead of just the first",
|
|
172
|
+
},
|
|
173
|
+
"dry_run": {
|
|
174
|
+
"type": "boolean",
|
|
175
|
+
"description": (
|
|
176
|
+
"If true, return a unified diff of the proposed change "
|
|
177
|
+
"without modifying the file. Defaults to false."
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
"required": ["file_path", "old_string", "new_string"],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def run(
|
|
185
|
+
self,
|
|
186
|
+
file_path: str,
|
|
187
|
+
old_string: str,
|
|
188
|
+
new_string: str,
|
|
189
|
+
replace_all: bool = False,
|
|
190
|
+
dry_run: bool = False,
|
|
191
|
+
) -> str: # type: ignore[override]
|
|
192
|
+
try:
|
|
193
|
+
path = validate_file_path(file_path, must_exist=True)
|
|
194
|
+
except (ValueError, FileNotFoundError) as e:
|
|
195
|
+
return f"Error: {e}"
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
content = path.read_text(encoding="utf-8")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return f"Error reading file: {e}"
|
|
201
|
+
|
|
202
|
+
if not replace_all:
|
|
203
|
+
count = content.count(old_string)
|
|
204
|
+
if count == 0:
|
|
205
|
+
return f"Error: old_string not found in {file_path}"
|
|
206
|
+
if count > 1:
|
|
207
|
+
return (
|
|
208
|
+
f"Error: old_string appears {count} times in {file_path}. "
|
|
209
|
+
"Use replace_all=True to replace all occurrences, "
|
|
210
|
+
"or provide more context to make it unique."
|
|
211
|
+
)
|
|
212
|
+
new_content = content.replace(old_string, new_string, 1)
|
|
213
|
+
else:
|
|
214
|
+
new_content = content.replace(old_string, new_string)
|
|
215
|
+
|
|
216
|
+
# Dry-run: show diff without touching the file
|
|
217
|
+
if dry_run:
|
|
218
|
+
diff = _unified_diff(content, new_content, path.name)
|
|
219
|
+
if not diff:
|
|
220
|
+
return "[dry-run] No changes — old_string and new_string are identical"
|
|
221
|
+
return f"[dry-run] Proposed changes to {file_path}:\n\n{diff}"
|
|
222
|
+
|
|
223
|
+
# Save undo info before editing
|
|
224
|
+
undo_push(file_path, content, "Edit")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
path.write_text(new_content, encoding="utf-8")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return f"Error writing file: {e}"
|
|
230
|
+
|
|
231
|
+
replacements = content.count(old_string) if replace_all else 1
|
|
232
|
+
return f"Applied {replacements} replacement(s) to {file_path}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class GlobTool(Tool):
|
|
236
|
+
name = "Glob"
|
|
237
|
+
description = "Find files matching a glob pattern. Supports ** and * wildcards."
|
|
238
|
+
parameters = {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"pattern": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"description": "Glob pattern (e.g., '**/*.py', 'src/**/*.ts')",
|
|
244
|
+
},
|
|
245
|
+
"path": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "Directory to search in (defaults to current directory)",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
"required": ["pattern"],
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def run(self, pattern: str, path: str | None = None) -> str: # type: ignore[override]
|
|
254
|
+
search_dir = path or os.getcwd()
|
|
255
|
+
try:
|
|
256
|
+
root = validate_file_path(search_dir, must_exist=True)
|
|
257
|
+
except (ValueError, FileNotFoundError) as e:
|
|
258
|
+
return f"Error: {e}"
|
|
259
|
+
|
|
260
|
+
if not root.is_dir():
|
|
261
|
+
return f"Error: not a directory: {search_dir}"
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
matches = sorted([p.relative_to(root) for p in root.rglob(pattern) if p.is_file()])
|
|
265
|
+
except Exception as e:
|
|
266
|
+
return f"Error during glob: {e}"
|
|
267
|
+
|
|
268
|
+
if not matches:
|
|
269
|
+
return f"No files matching '{pattern}' found in {search_dir}"
|
|
270
|
+
|
|
271
|
+
max_results = 200
|
|
272
|
+
if len(matches) > max_results:
|
|
273
|
+
lines = "\n".join(str(m) for m in matches[:max_results])
|
|
274
|
+
return f"{lines}\n... and {len(matches) - max_results} more"
|
|
275
|
+
return "\n".join(str(m) for m in matches)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class GrepTool(Tool):
|
|
279
|
+
name = "Grep"
|
|
280
|
+
description = "Search for a pattern in file contents. Supports regex."
|
|
281
|
+
parameters = {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"properties": {
|
|
284
|
+
"pattern": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"description": "Regular expression pattern to search for",
|
|
287
|
+
},
|
|
288
|
+
"path": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"description": "Directory or file to search in",
|
|
291
|
+
},
|
|
292
|
+
"glob": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"description": "File glob pattern to filter (e.g., '*.py')",
|
|
295
|
+
},
|
|
296
|
+
"output_mode": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"enum": ["content", "files_with_matches", "count"],
|
|
299
|
+
"description": "Output format",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
"required": ["pattern"],
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def run(
|
|
306
|
+
self,
|
|
307
|
+
pattern: str,
|
|
308
|
+
path: str | None = None,
|
|
309
|
+
glob: str | None = None,
|
|
310
|
+
output_mode: str = "content",
|
|
311
|
+
) -> str: # type: ignore[override]
|
|
312
|
+
search_path = path or os.getcwd()
|
|
313
|
+
try:
|
|
314
|
+
search_path_validated = validate_file_path(search_path, must_exist=True)
|
|
315
|
+
except (ValueError, FileNotFoundError) as e:
|
|
316
|
+
return f"Error: {e}"
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
regex = re.compile(pattern)
|
|
320
|
+
except re.error as e:
|
|
321
|
+
return f"Invalid regex: {e}"
|
|
322
|
+
|
|
323
|
+
results: list[str] = []
|
|
324
|
+
file_count = 0
|
|
325
|
+
total_matches = 0
|
|
326
|
+
limit = 200
|
|
327
|
+
|
|
328
|
+
files = []
|
|
329
|
+
if search_path_validated.is_file():
|
|
330
|
+
files = [str(search_path_validated)]
|
|
331
|
+
else:
|
|
332
|
+
for root, _dirs, fnames in os.walk(str(search_path_validated)):
|
|
333
|
+
for fname in fnames:
|
|
334
|
+
if glob and not self._matches_glob(fname, glob):
|
|
335
|
+
continue
|
|
336
|
+
files.append(os.path.join(root, fname))
|
|
337
|
+
|
|
338
|
+
for fpath in files:
|
|
339
|
+
try:
|
|
340
|
+
file_has_match = False
|
|
341
|
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
|
342
|
+
for i, line in enumerate(f, 1):
|
|
343
|
+
if regex.search(line.rstrip("\n")):
|
|
344
|
+
if not file_has_match:
|
|
345
|
+
file_has_match = True
|
|
346
|
+
file_count += 1
|
|
347
|
+
total_matches += 1
|
|
348
|
+
if output_mode == "content" and len(results) < limit:
|
|
349
|
+
rel = (
|
|
350
|
+
os.path.relpath(fpath, str(search_path_validated))
|
|
351
|
+
if search_path_validated.is_dir()
|
|
352
|
+
else os.path.basename(fpath)
|
|
353
|
+
)
|
|
354
|
+
results.append(f"{rel}:{i}:{line.rstrip()}")
|
|
355
|
+
elif output_mode == "files_with_matches" and (
|
|
356
|
+
len(results) == 0
|
|
357
|
+
or results[-1] != os.path.relpath(fpath, str(search_path_validated))
|
|
358
|
+
):
|
|
359
|
+
if len(results) < limit:
|
|
360
|
+
results.append(os.path.relpath(fpath, str(search_path_validated)))
|
|
361
|
+
except Exception:
|
|
362
|
+
_logger.debug("Error reading file %s during grep", fpath, exc_info=True)
|
|
363
|
+
|
|
364
|
+
if len(results) >= limit:
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
if output_mode == "count":
|
|
368
|
+
return f"{total_matches} matches in {file_count} files"
|
|
369
|
+
|
|
370
|
+
if not results:
|
|
371
|
+
return "No matches found"
|
|
372
|
+
|
|
373
|
+
text = "\n".join(results)
|
|
374
|
+
if total_matches > limit:
|
|
375
|
+
text += f"\n... and {total_matches - limit} more results"
|
|
376
|
+
return text
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _matches_glob(filename: str, pattern: str) -> bool:
|
|
380
|
+
import fnmatch
|
|
381
|
+
return fnmatch.fnmatch(filename, pattern)
|