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.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- 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,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)
|