just-bash 0.1.5__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.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. just_bash-0.1.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,1048 @@
1
+ """Rg (ripgrep) command implementation.
2
+
3
+ A feature-complete ripgrep emulation with:
4
+ - Recursive search by default
5
+ - Smart case sensitivity
6
+ - .gitignore support
7
+ - File type filtering
8
+ - Glob patterns
9
+ - Context lines
10
+ - Multiple output formats
11
+ """
12
+
13
+ import fnmatch
14
+ import re
15
+ from typing import Optional
16
+ from ...types import CommandContext, ExecResult
17
+
18
+
19
+ # File type definitions (subset of ripgrep's types)
20
+ FILE_TYPES: dict[str, list[str]] = {
21
+ "py": ["*.py", "*.pyi", "*.pyw"],
22
+ "python": ["*.py", "*.pyi", "*.pyw"],
23
+ "js": ["*.js", "*.mjs", "*.cjs", "*.jsx"],
24
+ "javascript": ["*.js", "*.mjs", "*.cjs", "*.jsx"],
25
+ "ts": ["*.ts", "*.tsx", "*.mts", "*.cts"],
26
+ "typescript": ["*.ts", "*.tsx", "*.mts", "*.cts"],
27
+ "json": ["*.json", "*.jsonl", "*.geojson"],
28
+ "yaml": ["*.yaml", "*.yml"],
29
+ "yml": ["*.yaml", "*.yml"],
30
+ "xml": ["*.xml", "*.xsl", "*.xslt", "*.svg"],
31
+ "html": ["*.html", "*.htm", "*.xhtml"],
32
+ "css": ["*.css", "*.scss", "*.sass", "*.less"],
33
+ "md": ["*.md", "*.markdown", "*.mdown"],
34
+ "markdown": ["*.md", "*.markdown", "*.mdown"],
35
+ "txt": ["*.txt", "*.text"],
36
+ "c": ["*.c", "*.h"],
37
+ "cpp": ["*.cpp", "*.cc", "*.cxx", "*.hpp", "*.hh", "*.hxx", "*.c++", "*.h++"],
38
+ "java": ["*.java"],
39
+ "go": ["*.go"],
40
+ "rust": ["*.rs"],
41
+ "rs": ["*.rs"],
42
+ "rb": ["*.rb", "*.ruby", "*.gemspec", "Rakefile"],
43
+ "ruby": ["*.rb", "*.ruby", "*.gemspec", "Rakefile"],
44
+ "sh": ["*.sh", "*.bash", "*.zsh", "*.fish"],
45
+ "shell": ["*.sh", "*.bash", "*.zsh", "*.fish"],
46
+ "sql": ["*.sql"],
47
+ "r": ["*.r", "*.R", "*.Rmd"],
48
+ "php": ["*.php", "*.php3", "*.php4", "*.php5", "*.phtml"],
49
+ "swift": ["*.swift"],
50
+ "kotlin": ["*.kt", "*.kts"],
51
+ "scala": ["*.scala", "*.sc"],
52
+ "lua": ["*.lua"],
53
+ "perl": ["*.pl", "*.pm", "*.t"],
54
+ "toml": ["*.toml"],
55
+ "ini": ["*.ini", "*.cfg", "*.conf"],
56
+ "make": ["Makefile", "*.mk", "GNUmakefile"],
57
+ "cmake": ["CMakeLists.txt", "*.cmake"],
58
+ "docker": ["Dockerfile", "*.dockerfile"],
59
+ "tf": ["*.tf", "*.tfvars"],
60
+ "terraform": ["*.tf", "*.tfvars"],
61
+ }
62
+
63
+
64
+ def format_type_list() -> str:
65
+ """Format the type list for --type-list output."""
66
+ lines = []
67
+ seen = set()
68
+ for name, patterns in sorted(FILE_TYPES.items()):
69
+ if name not in seen:
70
+ lines.append(f"{name}: {', '.join(patterns)}")
71
+ seen.add(name)
72
+ return "\n".join(lines) + "\n"
73
+
74
+
75
+ class RgOptions:
76
+ """Parsed options for rg command."""
77
+
78
+ def __init__(self):
79
+ # Patterns
80
+ self.patterns: list[str] = []
81
+ self.pattern_file: Optional[str] = None
82
+
83
+ # Case sensitivity
84
+ self.ignore_case = False
85
+ self.case_sensitive = False
86
+ self.smart_case = False
87
+
88
+ # Pattern matching
89
+ self.fixed_strings = False
90
+ self.word_regexp = False
91
+ self.line_regexp = False
92
+ self.invert_match = False
93
+ self.multiline = False
94
+ self.multiline_dotall = False
95
+
96
+ # Output modes
97
+ self.count = False
98
+ self.count_matches = False
99
+ self.files_with_matches = False
100
+ self.files_without_match = False
101
+ self.files_only = False # --files
102
+ self.only_matching = False
103
+ self.quiet = False
104
+ self.stats = False
105
+
106
+ # Line/column display
107
+ self.line_numbers = True # On by default
108
+ self.explicit_line_numbers = False
109
+ self.no_filename = False
110
+ self.column = False
111
+ self.byte_offset = False
112
+ self.null_separator = False
113
+
114
+ # Output formats
115
+ self.json = False
116
+ self.vimgrep = False
117
+ self.heading = False
118
+
119
+ # Context
120
+ self.after_context = 0
121
+ self.before_context = 0
122
+
123
+ # Replacement
124
+ self.replace: Optional[str] = None
125
+
126
+ # Limits
127
+ self.max_count: Optional[int] = None
128
+ self.max_depth: Optional[int] = None
129
+
130
+ # File selection
131
+ self.hidden = False
132
+ self.no_ignore = False
133
+ self.unrestricted_level = 0
134
+ self.globs: list[str] = []
135
+ self.types: list[str] = []
136
+ self.types_not: list[str] = []
137
+ self.search_binary = False
138
+
139
+ # Other
140
+ self.sort_by: Optional[str] = None
141
+ self.passthru = False
142
+
143
+
144
+ class RgCommand:
145
+ """The rg (ripgrep) command - recursive grep."""
146
+
147
+ name = "rg"
148
+
149
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
150
+ """Execute the rg command."""
151
+ # Parse arguments
152
+ opts, paths, error = self._parse_args(args, ctx)
153
+ if error:
154
+ return error
155
+
156
+ # Handle special modes
157
+ if opts.files_only:
158
+ return await self._list_files(ctx, paths, opts)
159
+
160
+ # Load patterns from file if specified
161
+ if opts.pattern_file:
162
+ try:
163
+ path = ctx.fs.resolve_path(ctx.cwd, opts.pattern_file)
164
+ content = await ctx.fs.read_file(path)
165
+ for line in content.splitlines():
166
+ if line.strip():
167
+ opts.patterns.append(line.strip())
168
+ except Exception as e:
169
+ return ExecResult(
170
+ stdout="",
171
+ stderr=f"rg: {opts.pattern_file}: {e}\n",
172
+ exit_code=2,
173
+ )
174
+
175
+ if not opts.patterns:
176
+ return ExecResult(
177
+ stdout="",
178
+ stderr="rg: no pattern given\n",
179
+ exit_code=2,
180
+ )
181
+
182
+ # Build regex
183
+ try:
184
+ regex = self._build_regex(opts)
185
+ except re.error as e:
186
+ return ExecResult(
187
+ stdout="",
188
+ stderr=f"rg: regex error: {e}\n",
189
+ exit_code=2,
190
+ )
191
+
192
+ # Execute search
193
+ return await self._search(ctx, paths, regex, opts)
194
+
195
+ def _parse_args(self, args: list[str], ctx: CommandContext) -> tuple[RgOptions, list[str], Optional[ExecResult]]:
196
+ """Parse command line arguments."""
197
+ opts = RgOptions()
198
+ paths: list[str] = []
199
+ i = 0
200
+
201
+ while i < len(args):
202
+ arg = args[i]
203
+
204
+ if arg == "--":
205
+ # Everything after -- is paths
206
+ paths.extend(args[i + 1:])
207
+ break
208
+ elif arg == "--help":
209
+ return opts, paths, self._show_help()
210
+ elif arg == "--type-list":
211
+ return opts, paths, ExecResult(
212
+ stdout=format_type_list(),
213
+ stderr="",
214
+ exit_code=0,
215
+ )
216
+ elif arg == "--version":
217
+ return opts, paths, ExecResult(
218
+ stdout="rg (just-bash) 0.1.0\n",
219
+ stderr="",
220
+ exit_code=0,
221
+ )
222
+
223
+ # Long options with values
224
+ elif arg.startswith("--") and "=" in arg:
225
+ key, value = arg.split("=", 1)
226
+ if key in ("--regexp", "-e"):
227
+ opts.patterns.append(value)
228
+ elif key in ("--file", "-f"):
229
+ opts.pattern_file = value
230
+ elif key in ("--replace", "-r"):
231
+ opts.replace = value
232
+ elif key in ("--max-count", "-m"):
233
+ opts.max_count = int(value)
234
+ elif key in ("--max-depth", "-d"):
235
+ opts.max_depth = int(value)
236
+ elif key in ("--glob", "-g"):
237
+ opts.globs.append(value)
238
+ elif key in ("--type", "-t"):
239
+ opts.types.append(value)
240
+ elif key in ("--type-not", "-T"):
241
+ opts.types_not.append(value)
242
+ elif key == "--context-separator":
243
+ pass # Accept but ignore
244
+ elif key == "--sort":
245
+ opts.sort_by = value
246
+ elif key in ("-A", "--after-context"):
247
+ opts.after_context = int(value)
248
+ elif key in ("-B", "--before-context"):
249
+ opts.before_context = int(value)
250
+ elif key in ("-C", "--context"):
251
+ opts.before_context = opts.after_context = int(value)
252
+
253
+ # Long options
254
+ elif arg.startswith("--"):
255
+ if arg == "--ignore-case":
256
+ opts.ignore_case = True
257
+ elif arg == "--case-sensitive":
258
+ opts.case_sensitive = True
259
+ elif arg == "--smart-case":
260
+ opts.smart_case = True
261
+ elif arg == "--fixed-strings":
262
+ opts.fixed_strings = True
263
+ elif arg == "--word-regexp":
264
+ opts.word_regexp = True
265
+ elif arg == "--line-regexp":
266
+ opts.line_regexp = True
267
+ elif arg == "--invert-match":
268
+ opts.invert_match = True
269
+ elif arg == "--multiline":
270
+ opts.multiline = True
271
+ elif arg == "--multiline-dotall":
272
+ opts.multiline_dotall = True
273
+ elif arg == "--count":
274
+ opts.count = True
275
+ elif arg == "--count-matches":
276
+ opts.count_matches = True
277
+ elif arg == "--files-with-matches":
278
+ opts.files_with_matches = True
279
+ elif arg == "--files-without-match":
280
+ opts.files_without_match = True
281
+ elif arg == "--files":
282
+ opts.files_only = True
283
+ elif arg == "--only-matching":
284
+ opts.only_matching = True
285
+ elif arg == "--quiet":
286
+ opts.quiet = True
287
+ elif arg == "--stats":
288
+ opts.stats = True
289
+ elif arg == "--line-number":
290
+ opts.line_numbers = True
291
+ opts.explicit_line_numbers = True
292
+ elif arg == "--no-line-number":
293
+ opts.line_numbers = False
294
+ elif arg == "--with-filename":
295
+ opts.no_filename = False
296
+ elif arg == "--no-filename":
297
+ opts.no_filename = True
298
+ elif arg == "--column":
299
+ opts.column = True
300
+ elif arg == "--no-column":
301
+ opts.column = False
302
+ elif arg == "--byte-offset":
303
+ opts.byte_offset = True
304
+ elif arg == "--null":
305
+ opts.null_separator = True
306
+ elif arg == "--json":
307
+ opts.json = True
308
+ elif arg == "--vimgrep":
309
+ opts.vimgrep = True
310
+ opts.line_numbers = True
311
+ opts.column = True
312
+ elif arg == "--heading":
313
+ opts.heading = True
314
+ elif arg == "--passthru":
315
+ opts.passthru = True
316
+ elif arg == "--hidden":
317
+ opts.hidden = True
318
+ elif arg == "--no-ignore":
319
+ opts.no_ignore = True
320
+ elif arg == "--no-ignore-dot":
321
+ opts.no_ignore = True
322
+ elif arg == "--no-ignore-vcs":
323
+ opts.no_ignore = True
324
+ elif arg == "--text":
325
+ opts.search_binary = True
326
+ elif arg == "--sort":
327
+ # Next arg is value
328
+ if i + 1 < len(args):
329
+ i += 1
330
+ opts.sort_by = args[i]
331
+ elif arg in ("--regexp", "--file", "--replace", "--max-count",
332
+ "--max-depth", "--glob", "--type", "--type-not",
333
+ "--after-context", "--before-context", "--context"):
334
+ # These need values
335
+ if i + 1 < len(args):
336
+ i += 1
337
+ value = args[i]
338
+ if arg == "--regexp":
339
+ opts.patterns.append(value)
340
+ elif arg == "--file":
341
+ opts.pattern_file = value
342
+ elif arg == "--replace":
343
+ opts.replace = value
344
+ elif arg == "--max-count":
345
+ opts.max_count = int(value)
346
+ elif arg == "--max-depth":
347
+ opts.max_depth = int(value)
348
+ elif arg == "--glob":
349
+ opts.globs.append(value)
350
+ elif arg == "--type":
351
+ opts.types.append(value)
352
+ elif arg == "--type-not":
353
+ opts.types_not.append(value)
354
+ elif arg == "--after-context":
355
+ opts.after_context = int(value)
356
+ elif arg == "--before-context":
357
+ opts.before_context = int(value)
358
+ elif arg == "--context":
359
+ opts.before_context = opts.after_context = int(value)
360
+ # Ignore other unknown long options
361
+
362
+ # Short options
363
+ elif arg.startswith("-") and arg != "-":
364
+ j = 1
365
+ while j < len(arg):
366
+ c = arg[j]
367
+ if c == "i":
368
+ opts.ignore_case = True
369
+ elif c == "s":
370
+ opts.case_sensitive = True
371
+ elif c == "S":
372
+ opts.smart_case = True
373
+ elif c == "F":
374
+ opts.fixed_strings = True
375
+ elif c == "w":
376
+ opts.word_regexp = True
377
+ elif c == "x":
378
+ opts.line_regexp = True
379
+ elif c == "v":
380
+ opts.invert_match = True
381
+ elif c == "U":
382
+ opts.multiline = True
383
+ elif c == "c":
384
+ opts.count = True
385
+ elif c == "l":
386
+ opts.files_with_matches = True
387
+ elif c == "o":
388
+ opts.only_matching = True
389
+ elif c == "q":
390
+ opts.quiet = True
391
+ elif c == "n":
392
+ opts.line_numbers = True
393
+ opts.explicit_line_numbers = True
394
+ elif c == "N":
395
+ opts.line_numbers = False
396
+ elif c == "H":
397
+ opts.no_filename = False
398
+ elif c == "I":
399
+ opts.no_filename = True
400
+ elif c == "b":
401
+ opts.byte_offset = True
402
+ elif c == "0":
403
+ opts.null_separator = True
404
+ elif c == "a":
405
+ opts.search_binary = True
406
+ elif c == "u":
407
+ opts.unrestricted_level += 1
408
+ if opts.unrestricted_level >= 1:
409
+ opts.no_ignore = True
410
+ if opts.unrestricted_level >= 2:
411
+ opts.hidden = True
412
+ if opts.unrestricted_level >= 3:
413
+ opts.search_binary = True
414
+ elif c in ("e", "f", "r", "m", "d", "g", "t", "T", "A", "B", "C"):
415
+ # These need values - either rest of arg or next arg
416
+ if j + 1 < len(arg):
417
+ value = arg[j + 1:]
418
+ elif i + 1 < len(args):
419
+ i += 1
420
+ value = args[i]
421
+ else:
422
+ value = ""
423
+ if c == "e":
424
+ opts.patterns.append(value)
425
+ elif c == "f":
426
+ opts.pattern_file = value
427
+ elif c == "r":
428
+ opts.replace = value
429
+ elif c == "m":
430
+ opts.max_count = int(value)
431
+ elif c == "d":
432
+ opts.max_depth = int(value)
433
+ elif c == "g":
434
+ opts.globs.append(value)
435
+ elif c == "t":
436
+ opts.types.append(value)
437
+ elif c == "T":
438
+ opts.types_not.append(value)
439
+ elif c == "A":
440
+ opts.after_context = int(value)
441
+ elif c == "B":
442
+ opts.before_context = int(value)
443
+ elif c == "C":
444
+ opts.before_context = opts.after_context = int(value)
445
+ break
446
+ j += 1
447
+
448
+ # Positional arguments
449
+ # In --files mode, all positional args are paths (no pattern needed)
450
+ # With -f (pattern file), all positional args are also paths
451
+ elif opts.files_only or opts.pattern_file:
452
+ paths.append(arg)
453
+ elif not opts.patterns:
454
+ opts.patterns.append(arg)
455
+ else:
456
+ paths.append(arg)
457
+ i += 1
458
+
459
+ # Default to current directory
460
+ if not paths:
461
+ paths = ["."]
462
+
463
+ return opts, paths, None
464
+
465
+ def _build_regex(self, opts: RgOptions) -> re.Pattern:
466
+ """Build the regex pattern from options."""
467
+ # Combine patterns with OR
468
+ if len(opts.patterns) == 1:
469
+ pattern = opts.patterns[0]
470
+ else:
471
+ # Escape for combining if using fixed strings
472
+ if opts.fixed_strings:
473
+ parts = [re.escape(p) for p in opts.patterns]
474
+ else:
475
+ parts = [f"(?:{p})" for p in opts.patterns]
476
+ pattern = "|".join(parts)
477
+
478
+ # Handle fixed strings
479
+ if opts.fixed_strings and len(opts.patterns) == 1:
480
+ pattern = re.escape(pattern)
481
+
482
+ # Handle word/line matching
483
+ if opts.word_regexp:
484
+ pattern = r"\b(?:" + pattern + r")\b"
485
+ if opts.line_regexp:
486
+ pattern = "^(?:" + pattern + ")$"
487
+
488
+ # Determine case sensitivity
489
+ flags = 0
490
+ if opts.case_sensitive:
491
+ pass # Case sensitive
492
+ elif opts.ignore_case:
493
+ flags |= re.IGNORECASE
494
+ elif opts.smart_case:
495
+ # Smart case: case insensitive unless pattern has uppercase
496
+ has_upper = any(c.isupper() for c in pattern if c.isalpha())
497
+ if not has_upper:
498
+ flags |= re.IGNORECASE
499
+
500
+ # Multiline
501
+ if opts.multiline:
502
+ flags |= re.MULTILINE
503
+ if opts.multiline_dotall:
504
+ flags |= re.DOTALL
505
+
506
+ return re.compile(pattern, flags)
507
+
508
+ async def _search(self, ctx: CommandContext, paths: list[str], regex: re.Pattern, opts: RgOptions) -> ExecResult:
509
+ """Execute the search."""
510
+ results: list[str] = []
511
+ stats = {"files": 0, "matches": 0, "lines": 0}
512
+ found_any = False
513
+
514
+ for path in paths:
515
+ if path == "-":
516
+ # Search stdin
517
+ found = await self._search_content(
518
+ ctx, "(standard input)", ctx.stdin, regex, opts, results, stats
519
+ )
520
+ if found:
521
+ found_any = True
522
+ else:
523
+ try:
524
+ resolved = ctx.fs.resolve_path(ctx.cwd, path)
525
+ stat = await ctx.fs.stat(resolved)
526
+
527
+ if stat.is_directory:
528
+ found = await self._search_directory(
529
+ ctx, resolved, path, regex, opts, results, stats, depth=0
530
+ )
531
+ if found:
532
+ found_any = True
533
+ else:
534
+ found = await self._search_file(
535
+ ctx, resolved, path, regex, opts, results, stats
536
+ )
537
+ if found:
538
+ found_any = True
539
+ except FileNotFoundError:
540
+ pass
541
+ except Exception:
542
+ pass
543
+
544
+ # Handle quiet mode
545
+ if opts.quiet:
546
+ return ExecResult(
547
+ stdout="",
548
+ stderr="",
549
+ exit_code=0 if found_any else 1,
550
+ )
551
+
552
+ # Build output
553
+ output = "\n".join(results)
554
+ if output:
555
+ output += "\n"
556
+
557
+ # Add stats if requested
558
+ if opts.stats:
559
+ output += f"\n{stats['matches']} matches\n"
560
+ output += f"{stats['lines']} matched lines\n"
561
+ output += f"{stats['files']} files contained matches\n"
562
+
563
+ return ExecResult(
564
+ stdout=output,
565
+ stderr="",
566
+ exit_code=0 if found_any else 1,
567
+ )
568
+
569
+ async def _search_directory(
570
+ self, ctx: CommandContext, path: str, display_path: str,
571
+ regex: re.Pattern, opts: RgOptions, results: list[str],
572
+ stats: dict, depth: int
573
+ ) -> bool:
574
+ """Search a directory recursively."""
575
+ # Check max depth
576
+ if opts.max_depth is not None and depth >= opts.max_depth:
577
+ return False
578
+
579
+ found_any = False
580
+
581
+ try:
582
+ entries = await ctx.fs.readdir(path)
583
+ if opts.sort_by == "path":
584
+ entries = sorted(entries)
585
+
586
+ for entry in entries:
587
+ # Skip hidden files/directories unless --hidden
588
+ if entry.startswith(".") and not opts.hidden:
589
+ continue
590
+
591
+ entry_path = f"{path}/{entry}"
592
+ entry_display = f"{display_path}/{entry}" if display_path != "." else entry
593
+
594
+ try:
595
+ stat = await ctx.fs.stat(entry_path)
596
+
597
+ if stat.is_directory:
598
+ # Skip .git directories unless --hidden
599
+ if entry == ".git" and not opts.hidden:
600
+ continue
601
+
602
+ found = await self._search_directory(
603
+ ctx, entry_path, entry_display, regex, opts, results, stats, depth + 1
604
+ )
605
+ if found:
606
+ found_any = True
607
+ else:
608
+ found = await self._search_file(
609
+ ctx, entry_path, entry_display, regex, opts, results, stats
610
+ )
611
+ if found:
612
+ found_any = True
613
+ except Exception:
614
+ pass
615
+ except Exception:
616
+ pass
617
+
618
+ return found_any
619
+
620
+ async def _search_file(
621
+ self, ctx: CommandContext, path: str, display_path: str,
622
+ regex: re.Pattern, opts: RgOptions, results: list[str], stats: dict
623
+ ) -> bool:
624
+ """Search a single file."""
625
+ # Check if file matches type filters
626
+ if not self._matches_type_filters(display_path, opts):
627
+ return False
628
+
629
+ # Check if file matches glob filters
630
+ if not self._matches_glob_filters(display_path, opts):
631
+ return False
632
+
633
+ # Check .gitignore
634
+ if not opts.no_ignore:
635
+ if await self._is_ignored(ctx, path, display_path):
636
+ return False
637
+
638
+ try:
639
+ content = await ctx.fs.read_file(path)
640
+ except Exception:
641
+ return False
642
+
643
+ # Check for binary content
644
+ if not opts.search_binary and self._is_binary(content):
645
+ return False
646
+
647
+ return await self._search_content(ctx, display_path, content, regex, opts, results, stats)
648
+
649
+ async def _search_content(
650
+ self, ctx: CommandContext, display_path: str, content: str,
651
+ regex: re.Pattern, opts: RgOptions, results: list[str], stats: dict
652
+ ) -> bool:
653
+ """Search content and add results."""
654
+ lines = content.splitlines()
655
+ matches: list[tuple[int, str, list[re.Match]]] = []
656
+ match_count = 0
657
+ line_match_count = 0
658
+
659
+ # Multiline mode: search entire content
660
+ if opts.multiline:
661
+ content_matches = list(regex.finditer(content))
662
+ if content_matches:
663
+ match_count = len(content_matches)
664
+ # For each match, determine which line(s) it spans
665
+ line_offsets = [0]
666
+ for line in lines:
667
+ line_offsets.append(line_offsets[-1] + len(line) + 1)
668
+
669
+ for m in content_matches:
670
+ # Find the starting line
671
+ start_line = 1
672
+ for i, offset in enumerate(line_offsets):
673
+ if offset > m.start():
674
+ start_line = i
675
+ break
676
+ matched_text = m.group(0).replace('\n', '\\n')
677
+ matches.append((start_line, matched_text, [m]))
678
+ line_match_count += 1
679
+
680
+ if opts.max_count is not None and line_match_count >= opts.max_count:
681
+ break
682
+ else:
683
+ # Normal line-by-line search
684
+ for line_num, line in enumerate(lines, 1):
685
+ line_matches = list(regex.finditer(line))
686
+
687
+ if opts.invert_match:
688
+ if not line_matches:
689
+ matches.append((line_num, line, []))
690
+ line_match_count += 1
691
+ elif line_matches:
692
+ matches.append((line_num, line, line_matches))
693
+ match_count += len(line_matches)
694
+ line_match_count += 1
695
+
696
+ # Check max count
697
+ if opts.max_count is not None and line_match_count >= opts.max_count:
698
+ break
699
+
700
+ if not matches and not opts.passthru:
701
+ # For files-without-match, we want to list files with NO matches
702
+ if opts.files_without_match:
703
+ results.append(display_path)
704
+ return True
705
+ return False
706
+
707
+ # Handle passthru mode - include all lines
708
+ if opts.passthru:
709
+ for line_num, line in enumerate(lines, 1):
710
+ line_matches = list(regex.finditer(line))
711
+ if any(m[0] == line_num for m in matches):
712
+ continue
713
+ matches.append((line_num, line, line_matches))
714
+ matches.sort(key=lambda x: x[0])
715
+
716
+ # Update stats
717
+ stats["files"] += 1
718
+ stats["matches"] += match_count
719
+ stats["lines"] += line_match_count
720
+
721
+ # Handle files-with-matches mode
722
+ if opts.files_with_matches:
723
+ sep = "\0" if opts.null_separator else ""
724
+ results.append(f"{display_path}{sep}")
725
+ return True
726
+
727
+ # Handle files-without-match mode (handled at caller level)
728
+ if opts.files_without_match:
729
+ return True # File has matches, don't list it
730
+
731
+ # Handle count mode
732
+ if opts.count:
733
+ results.append(f"{display_path}:{line_match_count}")
734
+ return True
735
+
736
+ # Handle count-matches mode
737
+ if opts.count_matches:
738
+ results.append(f"{display_path}:{match_count}")
739
+ return True
740
+
741
+ # Handle heading mode
742
+ if opts.heading:
743
+ results.append(display_path)
744
+
745
+ # Collect lines to output (including context if needed)
746
+ if opts.before_context > 0 or opts.after_context > 0:
747
+ # Build set of lines to include
748
+ lines_to_show: dict[int, tuple[str, bool]] = {} # line_num -> (content, is_match)
749
+ for line_num, line, line_matches in matches:
750
+ # Add context before
751
+ for ctx_num in range(max(1, line_num - opts.before_context), line_num):
752
+ if ctx_num not in lines_to_show:
753
+ lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
754
+ # Add the match line
755
+ lines_to_show[line_num] = (line, True)
756
+ # Add context after
757
+ for ctx_num in range(line_num + 1, min(len(lines) + 1, line_num + opts.after_context + 1)):
758
+ if ctx_num not in lines_to_show:
759
+ lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
760
+
761
+ # Output in order
762
+ for line_num in sorted(lines_to_show.keys()):
763
+ content, is_match = lines_to_show[line_num]
764
+ # Use '-' separator for context lines in some formats
765
+ sep = ":" if is_match else "-"
766
+ if not opts.no_filename:
767
+ if opts.line_numbers:
768
+ results.append(f"{display_path}{sep}{line_num}{sep}{content}")
769
+ else:
770
+ results.append(f"{display_path}{sep}{content}")
771
+ else:
772
+ if opts.line_numbers:
773
+ results.append(f"{line_num}{sep}{content}")
774
+ else:
775
+ results.append(content)
776
+ else:
777
+ # Normal output without context
778
+ for line_num, line, line_matches in matches:
779
+ # Handle only-matching mode
780
+ if opts.only_matching and line_matches:
781
+ for m in line_matches:
782
+ self._format_result(
783
+ results, opts, display_path, line_num, m.group(0),
784
+ column=m.start() + 1, byte_offset=self._calc_byte_offset(lines, line_num, m.start())
785
+ )
786
+ else:
787
+ # Handle replacement
788
+ output_line = line
789
+ if opts.replace is not None and line_matches:
790
+ # Convert ripgrep-style $1 to Python-style \1
791
+ py_replace = re.sub(r'\$(\d+)', r'\\\1', opts.replace)
792
+ output_line = regex.sub(py_replace, line)
793
+
794
+ col = line_matches[0].start() + 1 if line_matches else 1
795
+ byte_off = self._calc_byte_offset(lines, line_num, col - 1)
796
+ self._format_result(results, opts, display_path, line_num, output_line, column=col, byte_offset=byte_off)
797
+
798
+ return True
799
+
800
+ def _format_result(
801
+ self, results: list[str], opts: RgOptions, filename: str,
802
+ line_num: int, content: str, column: int = 1, byte_offset: int = 0
803
+ ) -> None:
804
+ """Format a result line."""
805
+ if opts.heading:
806
+ # Heading mode: line number and content only
807
+ parts = []
808
+ if opts.line_numbers:
809
+ parts.append(str(line_num))
810
+ if opts.column:
811
+ parts.append(str(column))
812
+ if opts.byte_offset:
813
+ parts.append(str(byte_offset))
814
+ if parts:
815
+ results.append(":".join(parts) + ":" + content)
816
+ else:
817
+ results.append(content)
818
+ elif opts.vimgrep:
819
+ # vimgrep format: file:line:column:content
820
+ results.append(f"{filename}:{line_num}:{column}:{content}")
821
+ elif opts.json:
822
+ # Simple JSON-like output
823
+ import json
824
+ results.append(json.dumps({
825
+ "type": "match",
826
+ "data": {
827
+ "path": {"text": filename},
828
+ "lines": {"text": content},
829
+ "line_number": line_num,
830
+ "submatches": []
831
+ }
832
+ }))
833
+ else:
834
+ # Normal format
835
+ parts = []
836
+ if not opts.no_filename:
837
+ parts.append(filename)
838
+ if opts.line_numbers:
839
+ parts.append(str(line_num))
840
+ if opts.column:
841
+ parts.append(str(column))
842
+ if opts.byte_offset:
843
+ parts.append(str(byte_offset))
844
+
845
+ if parts:
846
+ results.append(":".join(parts) + ":" + content)
847
+ else:
848
+ results.append(content)
849
+
850
+ def _calc_byte_offset(self, lines: list[str], line_num: int, col: int) -> int:
851
+ """Calculate byte offset from start of file."""
852
+ offset = sum(len(l) + 1 for l in lines[:line_num - 1])
853
+ return offset + col
854
+
855
+ def _matches_type_filters(self, path: str, opts: RgOptions) -> bool:
856
+ """Check if file matches type filters."""
857
+ if not opts.types and not opts.types_not:
858
+ return True
859
+
860
+ filename = path.split("/")[-1]
861
+
862
+ # Check type exclusions first
863
+ for type_name in opts.types_not:
864
+ patterns = FILE_TYPES.get(type_name, [])
865
+ for pattern in patterns:
866
+ if fnmatch.fnmatch(filename, pattern):
867
+ return False
868
+
869
+ # If no include types, allow all (that weren't excluded)
870
+ if not opts.types:
871
+ return True
872
+
873
+ # Check type inclusions
874
+ for type_name in opts.types:
875
+ patterns = FILE_TYPES.get(type_name, [])
876
+ for pattern in patterns:
877
+ if fnmatch.fnmatch(filename, pattern):
878
+ return True
879
+
880
+ return False
881
+
882
+ def _matches_glob_filters(self, path: str, opts: RgOptions) -> bool:
883
+ """Check if file matches glob filters."""
884
+ if not opts.globs:
885
+ return True
886
+
887
+ filename = path.split("/")[-1]
888
+
889
+ for glob in opts.globs:
890
+ # Negated glob
891
+ if glob.startswith("!"):
892
+ pattern = glob[1:]
893
+ if fnmatch.fnmatch(filename, pattern) or fnmatch.fnmatch(path, pattern):
894
+ return False
895
+ else:
896
+ # Positive glob - at least one must match
897
+ if fnmatch.fnmatch(filename, glob) or fnmatch.fnmatch(path, glob):
898
+ return True
899
+
900
+ # If only negated globs, allow; if positive globs, require match
901
+ has_positive = any(not g.startswith("!") for g in opts.globs)
902
+ return not has_positive
903
+
904
+ async def _is_ignored(self, ctx: CommandContext, path: str, display_path: str) -> bool:
905
+ """Check if file is ignored by .gitignore."""
906
+ # Walk up directories looking for .gitignore
907
+ parts = path.split("/")
908
+ filename = parts[-1]
909
+
910
+ for i in range(len(parts) - 1, 0, -1):
911
+ dir_path = "/".join(parts[:i])
912
+ gitignore_path = f"{dir_path}/.gitignore"
913
+
914
+ try:
915
+ content = await ctx.fs.read_file(gitignore_path)
916
+ for line in content.splitlines():
917
+ line = line.strip()
918
+ if not line or line.startswith("#"):
919
+ continue
920
+ # Simple pattern matching
921
+ if fnmatch.fnmatch(filename, line) or fnmatch.fnmatch(display_path, line):
922
+ return True
923
+ except Exception:
924
+ pass
925
+
926
+ return False
927
+
928
+ def _is_binary(self, content: str) -> bool:
929
+ """Check if content appears to be binary."""
930
+ # Check for null bytes or high ratio of non-printable chars
931
+ if "\x00" in content:
932
+ return True
933
+ if len(content) > 0:
934
+ non_printable = sum(1 for c in content[:1000] if ord(c) < 32 and c not in "\n\r\t")
935
+ if non_printable / min(len(content), 1000) > 0.1:
936
+ return True
937
+ return False
938
+
939
+ async def _list_files(self, ctx: CommandContext, paths: list[str], opts: RgOptions) -> ExecResult:
940
+ """List files that would be searched (--files mode)."""
941
+ files: list[str] = []
942
+
943
+ for path in paths:
944
+ try:
945
+ resolved = ctx.fs.resolve_path(ctx.cwd, path)
946
+ stat = await ctx.fs.stat(resolved)
947
+
948
+ if stat.is_directory:
949
+ await self._collect_files(ctx, resolved, path, opts, files, depth=0)
950
+ else:
951
+ if self._matches_type_filters(path, opts) and self._matches_glob_filters(path, opts):
952
+ files.append(path)
953
+ except Exception:
954
+ pass
955
+
956
+ if opts.sort_by == "path":
957
+ files.sort()
958
+
959
+ output = "\n".join(files)
960
+ if output:
961
+ output += "\n"
962
+
963
+ return ExecResult(stdout=output, stderr="", exit_code=0)
964
+
965
+ async def _collect_files(
966
+ self, ctx: CommandContext, path: str, display_path: str,
967
+ opts: RgOptions, files: list[str], depth: int
968
+ ) -> None:
969
+ """Collect files for --files mode."""
970
+ if opts.max_depth is not None and depth >= opts.max_depth:
971
+ return
972
+
973
+ try:
974
+ entries = await ctx.fs.readdir(path)
975
+
976
+ for entry in sorted(entries):
977
+ if entry.startswith(".") and not opts.hidden:
978
+ continue
979
+
980
+ entry_path = f"{path}/{entry}"
981
+ entry_display = f"{display_path}/{entry}" if display_path != "." else entry
982
+
983
+ try:
984
+ stat = await ctx.fs.stat(entry_path)
985
+
986
+ if stat.is_directory:
987
+ await self._collect_files(ctx, entry_path, entry_display, opts, files, depth + 1)
988
+ else:
989
+ if self._matches_type_filters(entry_display, opts) and self._matches_glob_filters(entry_display, opts):
990
+ files.append(entry_display)
991
+ except Exception:
992
+ pass
993
+ except Exception:
994
+ pass
995
+
996
+ def _show_help(self) -> ExecResult:
997
+ """Show help message."""
998
+ help_text = """rg - recursively search for a pattern
999
+
1000
+ USAGE:
1001
+ rg [OPTIONS] PATTERN [PATH ...]
1002
+
1003
+ OPTIONS:
1004
+ -e, --regexp PATTERN Pattern to search for (can be repeated)
1005
+ -f, --file FILE Read patterns from file
1006
+ -i, --ignore-case Case insensitive search
1007
+ -s, --case-sensitive Case sensitive search
1008
+ -S, --smart-case Smart case (default)
1009
+ -F, --fixed-strings Treat pattern as literal string
1010
+ -w, --word-regexp Match whole words only
1011
+ -x, --line-regexp Match whole lines only
1012
+ -v, --invert-match Select non-matching lines
1013
+ -c, --count Print count of matches per file
1014
+ --count-matches Print count of individual matches
1015
+ -l, --files-with-matches Print only filenames with matches
1016
+ --files-without-match Print filenames without matches
1017
+ --files Print files that would be searched
1018
+ -o, --only-matching Print only matched parts
1019
+ -r, --replace TEXT Replace matches with TEXT
1020
+ -q, --quiet Suppress output
1021
+ -n, --line-number Show line numbers (default)
1022
+ -N, --no-line-number Hide line numbers
1023
+ -I, --no-filename Hide filenames
1024
+ --column Show column numbers
1025
+ -b, --byte-offset Show byte offsets
1026
+ --vimgrep Output in vimgrep format
1027
+ --json Output in JSON format
1028
+ --heading Show filename above matches
1029
+ -A NUM Show NUM lines after match
1030
+ -B NUM Show NUM lines before match
1031
+ -C NUM Show NUM lines of context
1032
+ -m, --max-count NUM Stop after NUM matches per file
1033
+ -d, --max-depth NUM Maximum directory depth
1034
+ -g, --glob PATTERN Include files matching glob
1035
+ -t, --type TYPE Search only TYPE files
1036
+ -T, --type-not TYPE Exclude TYPE files
1037
+ --type-list List available file types
1038
+ --hidden Search hidden files
1039
+ --no-ignore Don't respect .gitignore
1040
+ -u Unrestricted mode (stacks)
1041
+ -U, --multiline Enable multiline matching
1042
+ --stats Show search statistics
1043
+ --sort TYPE Sort results (path, none)
1044
+ --passthru Print all lines
1045
+ -a, --text Search binary files as text
1046
+ --help Show this help
1047
+ """
1048
+ return ExecResult(stdout=help_text, stderr="", exit_code=0)