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,160 @@
1
+ """Fold command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class FoldCommand:
7
+ """The fold command - wrap lines to fit in specified width."""
8
+
9
+ name = "fold"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the fold command."""
13
+ width = 80
14
+ break_spaces = False
15
+ count_bytes = False
16
+ files: list[str] = []
17
+
18
+ i = 0
19
+ while i < len(args):
20
+ arg = args[i]
21
+ if arg == "--help":
22
+ return ExecResult(
23
+ stdout="Usage: fold [OPTION]... [FILE]...\nWrap lines to fit in specified width.\n",
24
+ stderr="",
25
+ exit_code=0,
26
+ )
27
+ elif arg in ("-s", "--spaces"):
28
+ break_spaces = True
29
+ elif arg in ("-b", "--bytes"):
30
+ count_bytes = True
31
+ elif arg == "-w" and i + 1 < len(args):
32
+ i += 1
33
+ try:
34
+ width = int(args[i])
35
+ except ValueError:
36
+ return ExecResult(
37
+ stdout="",
38
+ stderr=f"fold: invalid width: '{args[i]}'\n",
39
+ exit_code=1,
40
+ )
41
+ elif arg.startswith("-w"):
42
+ try:
43
+ width = int(arg[2:])
44
+ except ValueError:
45
+ return ExecResult(
46
+ stdout="",
47
+ stderr=f"fold: invalid width: '{arg[2:]}'\n",
48
+ exit_code=1,
49
+ )
50
+ elif arg.startswith("--width="):
51
+ try:
52
+ width = int(arg[8:])
53
+ except ValueError:
54
+ return ExecResult(
55
+ stdout="",
56
+ stderr=f"fold: invalid width: '{arg[8:]}'\n",
57
+ exit_code=1,
58
+ )
59
+ elif arg == "--":
60
+ files.extend(args[i + 1:])
61
+ break
62
+ elif arg.startswith("-") and len(arg) > 1:
63
+ # Could be -N for width
64
+ try:
65
+ width = int(arg[1:])
66
+ except ValueError:
67
+ return ExecResult(
68
+ stdout="",
69
+ stderr=f"fold: invalid option -- '{arg[1]}'\n",
70
+ exit_code=1,
71
+ )
72
+ else:
73
+ files.append(arg)
74
+ i += 1
75
+
76
+ if width <= 0:
77
+ return ExecResult(
78
+ stdout="",
79
+ stderr="fold: width must be greater than 0\n",
80
+ exit_code=1,
81
+ )
82
+
83
+ # Read from stdin if no files
84
+ if not files:
85
+ content = ctx.stdin
86
+ result = self._fold_content(content, width, break_spaces)
87
+ return ExecResult(stdout=result, stderr="", exit_code=0)
88
+
89
+ stdout_parts = []
90
+ stderr = ""
91
+ exit_code = 0
92
+
93
+ for file in files:
94
+ try:
95
+ if file == "-":
96
+ content = ctx.stdin
97
+ else:
98
+ path = ctx.fs.resolve_path(ctx.cwd, file)
99
+ content = await ctx.fs.read_file(path)
100
+
101
+ result = self._fold_content(content, width, break_spaces)
102
+ stdout_parts.append(result)
103
+
104
+ except FileNotFoundError:
105
+ stderr += f"fold: {file}: No such file or directory\n"
106
+ exit_code = 1
107
+
108
+ return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
109
+
110
+ def _fold_content(self, content: str, width: int, break_spaces: bool) -> str:
111
+ """Fold content to specified width."""
112
+ lines = content.split("\n")
113
+ result_lines = []
114
+
115
+ for line in lines:
116
+ result_lines.extend(self._fold_line(line, width, break_spaces))
117
+
118
+ return "\n".join(result_lines)
119
+
120
+ def _fold_line(self, line: str, width: int, break_spaces: bool) -> list[str]:
121
+ """Fold a single line."""
122
+ if len(line) <= width:
123
+ return [line]
124
+
125
+ result = []
126
+
127
+ if break_spaces:
128
+ # Break at spaces when possible
129
+ current = ""
130
+ for word in line.split(" "):
131
+ if not current:
132
+ current = word
133
+ elif len(current) + 1 + len(word) <= width:
134
+ current += " " + word
135
+ else:
136
+ # Current line is full
137
+ if len(current) > width:
138
+ # Word itself is too long, break it
139
+ while len(current) > width:
140
+ result.append(current[:width])
141
+ current = current[width:]
142
+ if current:
143
+ result.append(current)
144
+ current = word
145
+
146
+ # Handle remaining content
147
+ while len(current) > width:
148
+ result.append(current[:width])
149
+ current = current[width:]
150
+ if current:
151
+ result.append(current)
152
+ else:
153
+ # Simple character-based folding
154
+ while len(line) > width:
155
+ result.append(line[:width])
156
+ line = line[width:]
157
+ if line:
158
+ result.append(line)
159
+
160
+ return result if result else [""]
@@ -0,0 +1,5 @@
1
+ """Grep command implementation."""
2
+
3
+ from .grep import GrepCommand, FgrepCommand, EgrepCommand
4
+
5
+ __all__ = ["GrepCommand", "FgrepCommand", "EgrepCommand"]
@@ -0,0 +1,418 @@
1
+ """Grep command implementation.
2
+
3
+ Usage: grep [OPTION]... PATTERN [FILE]...
4
+
5
+ Search for PATTERN in each FILE.
6
+ With no FILE, or when FILE is -, read standard input.
7
+
8
+ Options:
9
+ -i, --ignore-case ignore case distinctions
10
+ -v, --invert-match select non-matching lines
11
+ -c, --count print only a count of matching lines per FILE
12
+ -l, --files-with-matches print only names of FILEs with matches
13
+ -L, --files-without-match print only names of FILEs without matches
14
+ -n, --line-number print line number with output lines
15
+ -H, --with-filename print the file name for each match
16
+ -h, --no-filename suppress the file name prefix on output
17
+ -o, --only-matching show only the part of a line matching PATTERN
18
+ -q, --quiet, --silent suppress all normal output
19
+ -r, -R, --recursive recursively search directories
20
+ -E, --extended-regexp PATTERN is an extended regular expression
21
+ -F, --fixed-strings PATTERN is a set of newline-separated strings
22
+ -w, --word-regexp match only whole words
23
+ -x, --line-regexp match only whole lines
24
+ -A NUM, --after-context=NUM print NUM lines of trailing context
25
+ -B NUM, --before-context=NUM print NUM lines of leading context
26
+ -C NUM, --context=NUM print NUM lines of output context
27
+ -m NUM, --max-count=NUM stop after NUM matches
28
+ -e PATTERN use PATTERN for matching
29
+ --include=GLOB search only files matching GLOB
30
+ --exclude=GLOB skip files matching GLOB
31
+ """
32
+
33
+ import fnmatch
34
+ import re
35
+ from ...types import CommandContext, ExecResult
36
+
37
+
38
+ class GrepCommand:
39
+ """The grep command."""
40
+
41
+ name = "grep"
42
+
43
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
44
+ """Execute the grep command."""
45
+ ignore_case = False
46
+ invert_match = False
47
+ count_only = False
48
+ files_with_matches = False
49
+ files_without_match = False
50
+ line_numbers = False
51
+ with_filename = None # None means auto-detect based on file count
52
+ only_matching = False
53
+ quiet = False
54
+ recursive = False
55
+ extended_regexp = False
56
+ fixed_strings = False
57
+ word_regexp = False
58
+ line_regexp = False
59
+ after_context = 0
60
+ before_context = 0
61
+ max_count = None
62
+ include_globs: list[str] = []
63
+ exclude_globs: list[str] = []
64
+ patterns: list[str] = []
65
+
66
+ pattern = None
67
+ files: list[str] = []
68
+
69
+ # Parse arguments
70
+ i = 0
71
+ while i < len(args):
72
+ arg = args[i]
73
+ if arg == "--":
74
+ if pattern is None and i + 1 < len(args):
75
+ pattern = args[i + 1]
76
+ files.extend(args[i + 2:])
77
+ else:
78
+ files.extend(args[i + 1:])
79
+ break
80
+ elif arg.startswith("--"):
81
+ if arg == "--ignore-case":
82
+ ignore_case = True
83
+ elif arg == "--invert-match":
84
+ invert_match = True
85
+ elif arg == "--count":
86
+ count_only = True
87
+ elif arg == "--files-with-matches":
88
+ files_with_matches = True
89
+ elif arg == "--files-without-match":
90
+ files_without_match = True
91
+ elif arg == "--line-number":
92
+ line_numbers = True
93
+ elif arg == "--with-filename":
94
+ with_filename = True
95
+ elif arg == "--no-filename":
96
+ with_filename = False
97
+ elif arg == "--only-matching":
98
+ only_matching = True
99
+ elif arg == "--quiet" or arg == "--silent":
100
+ quiet = True
101
+ elif arg == "--recursive":
102
+ recursive = True
103
+ elif arg == "--extended-regexp":
104
+ extended_regexp = True
105
+ elif arg == "--perl-regexp":
106
+ # Perl-compatible regex - Python's re is already PCRE-like
107
+ extended_regexp = True
108
+ elif arg == "--fixed-strings":
109
+ fixed_strings = True
110
+ elif arg == "--word-regexp":
111
+ word_regexp = True
112
+ elif arg == "--line-regexp":
113
+ line_regexp = True
114
+ elif arg.startswith("--after-context="):
115
+ after_context = int(arg.split("=", 1)[1])
116
+ elif arg.startswith("--before-context="):
117
+ before_context = int(arg.split("=", 1)[1])
118
+ elif arg.startswith("--context="):
119
+ ctx_val = int(arg.split("=", 1)[1])
120
+ before_context = after_context = ctx_val
121
+ elif arg.startswith("--max-count="):
122
+ max_count = int(arg.split("=", 1)[1])
123
+ elif arg.startswith("--include="):
124
+ include_globs.append(arg.split("=", 1)[1])
125
+ elif arg.startswith("--exclude="):
126
+ exclude_globs.append(arg.split("=", 1)[1])
127
+ else:
128
+ return ExecResult(
129
+ stdout="",
130
+ stderr=f"grep: unrecognized option '{arg}'\n",
131
+ exit_code=2,
132
+ )
133
+ elif arg.startswith("-") and arg != "-":
134
+ # Handle options that take arguments
135
+ if arg in ("-A", "-B", "-C", "-m", "-e"):
136
+ if i + 1 >= len(args):
137
+ return ExecResult(
138
+ stdout="",
139
+ stderr=f"grep: option requires an argument -- '{arg[-1]}'\n",
140
+ exit_code=2,
141
+ )
142
+ i += 1
143
+ val = args[i]
144
+ if arg == "-A":
145
+ after_context = int(val)
146
+ elif arg == "-B":
147
+ before_context = int(val)
148
+ elif arg == "-C":
149
+ before_context = after_context = int(val)
150
+ elif arg == "-m":
151
+ max_count = int(val)
152
+ elif arg == "-e":
153
+ patterns.append(val)
154
+ else:
155
+ for c in arg[1:]:
156
+ if c == 'i':
157
+ ignore_case = True
158
+ elif c == 'v':
159
+ invert_match = True
160
+ elif c == 'c':
161
+ count_only = True
162
+ elif c == 'l':
163
+ files_with_matches = True
164
+ elif c == 'L':
165
+ files_without_match = True
166
+ elif c == 'n':
167
+ line_numbers = True
168
+ elif c == 'H':
169
+ with_filename = True
170
+ elif c == 'h':
171
+ with_filename = False
172
+ elif c == 'o':
173
+ only_matching = True
174
+ elif c == 'q':
175
+ quiet = True
176
+ elif c == 'r' or c == 'R':
177
+ recursive = True
178
+ elif c == 'E':
179
+ extended_regexp = True
180
+ elif c == 'F':
181
+ fixed_strings = True
182
+ elif c == 'P':
183
+ # Perl-compatible regex - Python's re is already PCRE-like
184
+ extended_regexp = True
185
+ elif c == 'w':
186
+ word_regexp = True
187
+ elif c == 'x':
188
+ line_regexp = True
189
+ else:
190
+ return ExecResult(
191
+ stdout="",
192
+ stderr=f"grep: invalid option -- '{c}'\n",
193
+ exit_code=2,
194
+ )
195
+ elif pattern is None and not patterns:
196
+ pattern = arg
197
+ else:
198
+ files.append(arg)
199
+ i += 1
200
+
201
+ # Use -e patterns if provided, otherwise use positional pattern
202
+ if patterns:
203
+ if pattern:
204
+ # If pattern was set, it's actually a file
205
+ files.insert(0, pattern)
206
+ pattern = "|".join(f"({p})" for p in patterns)
207
+ elif pattern is None:
208
+ return ExecResult(
209
+ stdout="",
210
+ stderr="grep: pattern not specified\n",
211
+ exit_code=2,
212
+ )
213
+
214
+ # Default to stdin
215
+ if not files:
216
+ files = ["-"]
217
+
218
+ # Build regex pattern
219
+ try:
220
+ if fixed_strings:
221
+ # Escape all regex metacharacters
222
+ pattern = re.escape(pattern)
223
+ if word_regexp:
224
+ pattern = r'\b' + pattern + r'\b'
225
+ if line_regexp:
226
+ pattern = '^' + pattern + '$'
227
+
228
+ flags = re.IGNORECASE if ignore_case else 0
229
+ regex = re.compile(pattern, flags)
230
+ except re.error as e:
231
+ return ExecResult(
232
+ stdout="",
233
+ stderr=f"grep: invalid pattern '{pattern}': {e}\n",
234
+ exit_code=2,
235
+ )
236
+
237
+ # Expand files for recursive search
238
+ expanded_files = []
239
+ for file in files:
240
+ if file == "-":
241
+ expanded_files.append(file)
242
+ else:
243
+ path = ctx.fs.resolve_path(ctx.cwd, file)
244
+ try:
245
+ if await ctx.fs.is_directory(path):
246
+ if recursive:
247
+ # Get all files recursively
248
+ all_files = await self._get_files_recursive(ctx, path)
249
+ expanded_files.extend(all_files)
250
+ else:
251
+ expanded_files.append(file) # Will error later
252
+ else:
253
+ expanded_files.append(file)
254
+ except FileNotFoundError:
255
+ expanded_files.append(file) # Will error later
256
+
257
+ # Filter by include/exclude globs
258
+ if include_globs or exclude_globs:
259
+ filtered_files = []
260
+ for file in expanded_files:
261
+ if file == "-":
262
+ filtered_files.append(file)
263
+ continue
264
+ filename = file.split("/")[-1]
265
+ # Check include
266
+ if include_globs:
267
+ if not any(fnmatch.fnmatch(filename, g) for g in include_globs):
268
+ continue
269
+ # Check exclude
270
+ if exclude_globs:
271
+ if any(fnmatch.fnmatch(filename, g) for g in exclude_globs):
272
+ continue
273
+ filtered_files.append(file)
274
+ expanded_files = filtered_files
275
+
276
+ # Auto-detect filename display
277
+ if with_filename is None:
278
+ with_filename = len(expanded_files) > 1
279
+
280
+ stdout = ""
281
+ stderr = ""
282
+ found_match = False
283
+ total_matches = 0
284
+
285
+ for file in expanded_files:
286
+ if max_count is not None and total_matches >= max_count:
287
+ break
288
+
289
+ try:
290
+ if file == "-":
291
+ content = ctx.stdin
292
+ else:
293
+ path = ctx.fs.resolve_path(ctx.cwd, file)
294
+ content = await ctx.fs.read_file(path)
295
+
296
+ lines = content.split("\n")
297
+ # Handle trailing empty line from split
298
+ if lines and lines[-1] == "":
299
+ lines = lines[:-1]
300
+
301
+ match_count = 0
302
+ file_has_match = False
303
+ matched_line_nums: list[int] = []
304
+
305
+ # First pass: find all matching lines
306
+ for line_num, line in enumerate(lines, 1):
307
+ if max_count is not None and total_matches >= max_count:
308
+ break
309
+
310
+ match = regex.search(line)
311
+ matches_pattern = bool(match)
312
+
313
+ if invert_match:
314
+ matches_pattern = not matches_pattern
315
+
316
+ if matches_pattern:
317
+ match_count += 1
318
+ total_matches += 1
319
+ file_has_match = True
320
+ found_match = True
321
+ matched_line_nums.append(line_num)
322
+
323
+ if quiet:
324
+ return ExecResult(stdout="", stderr="", exit_code=0)
325
+
326
+ # Second pass: output with context
327
+ if not count_only and not files_with_matches and not files_without_match:
328
+ output_lines: set[int] = set()
329
+ for ln in matched_line_nums:
330
+ # Add before context
331
+ for b in range(max(1, ln - before_context), ln):
332
+ output_lines.add(b)
333
+ # Add match line
334
+ output_lines.add(ln)
335
+ # Add after context
336
+ for a in range(ln + 1, min(len(lines) + 1, ln + after_context + 1)):
337
+ output_lines.add(a)
338
+
339
+ prev_line = 0
340
+ for line_num in sorted(output_lines):
341
+ if line_num < 1 or line_num > len(lines):
342
+ continue
343
+ # Add separator for non-contiguous blocks
344
+ if before_context or after_context:
345
+ if prev_line > 0 and line_num > prev_line + 1:
346
+ stdout += "--\n"
347
+ prev_line = line_num
348
+
349
+ line = lines[line_num - 1]
350
+ is_match = line_num in matched_line_nums
351
+ match = regex.search(line) if is_match else None
352
+
353
+ if only_matching and match and not invert_match and is_match:
354
+ output = match.group(0)
355
+ else:
356
+ output = line
357
+
358
+ parts = []
359
+ if with_filename:
360
+ parts.append(f"{file}:")
361
+ if line_numbers:
362
+ sep = ":" if is_match else "-"
363
+ parts.append(f"{line_num}{sep}")
364
+ parts.append(output)
365
+ stdout += "".join(parts) + "\n"
366
+
367
+ if count_only:
368
+ if with_filename:
369
+ stdout += f"{file}:{match_count}\n"
370
+ else:
371
+ stdout += f"{match_count}\n"
372
+ elif files_with_matches and file_has_match:
373
+ stdout += f"{file}\n"
374
+ elif files_without_match and not file_has_match:
375
+ stdout += f"{file}\n"
376
+
377
+ except FileNotFoundError:
378
+ stderr += f"grep: {file}: No such file or directory\n"
379
+ except IsADirectoryError:
380
+ stderr += f"grep: {file}: Is a directory\n"
381
+
382
+ exit_code = 0 if found_match else 1
383
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
384
+
385
+ async def _get_files_recursive(self, ctx: CommandContext, path: str) -> list[str]:
386
+ """Get all files in a directory recursively."""
387
+ files = []
388
+ try:
389
+ entries = await ctx.fs.readdir(path)
390
+ for entry in entries:
391
+ full_path = f"{path}/{entry}"
392
+ if await ctx.fs.is_directory(full_path):
393
+ files.extend(await self._get_files_recursive(ctx, full_path))
394
+ else:
395
+ files.append(full_path)
396
+ except Exception:
397
+ pass
398
+ return sorted(files)
399
+
400
+
401
+ class FgrepCommand(GrepCommand):
402
+ """The fgrep command - grep with fixed strings."""
403
+
404
+ name = "fgrep"
405
+
406
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
407
+ """Execute fgrep (grep -F)."""
408
+ return await super().execute(["-F"] + args, ctx)
409
+
410
+
411
+ class EgrepCommand(GrepCommand):
412
+ """The egrep command - grep with extended regexp."""
413
+
414
+ name = "egrep"
415
+
416
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
417
+ """Execute egrep (grep -E)."""
418
+ return await super().execute(["-E"] + args, ctx)
@@ -0,0 +1,5 @@
1
+ """Head command implementation."""
2
+
3
+ from .head import HeadCommand
4
+
5
+ __all__ = ["HeadCommand"]