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,106 @@
|
|
|
1
|
+
"""Rm command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: rm [OPTION]... FILE...
|
|
4
|
+
|
|
5
|
+
Remove (unlink) the FILE(s).
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-f, --force ignore nonexistent files and arguments
|
|
9
|
+
-r, -R, --recursive remove directories and their contents recursively
|
|
10
|
+
-v, --verbose explain what is being done
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ...types import CommandContext, ExecResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RmCommand:
|
|
17
|
+
"""The rm command."""
|
|
18
|
+
|
|
19
|
+
name = "rm"
|
|
20
|
+
|
|
21
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
22
|
+
"""Execute the rm command."""
|
|
23
|
+
force = False
|
|
24
|
+
recursive = False
|
|
25
|
+
verbose = False
|
|
26
|
+
files: list[str] = []
|
|
27
|
+
|
|
28
|
+
# Parse arguments
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(args):
|
|
31
|
+
arg = args[i]
|
|
32
|
+
if arg == "--":
|
|
33
|
+
files.extend(args[i + 1:])
|
|
34
|
+
break
|
|
35
|
+
elif arg.startswith("--"):
|
|
36
|
+
if arg == "--force":
|
|
37
|
+
force = True
|
|
38
|
+
elif arg == "--recursive":
|
|
39
|
+
recursive = True
|
|
40
|
+
elif arg == "--verbose":
|
|
41
|
+
verbose = True
|
|
42
|
+
else:
|
|
43
|
+
return ExecResult(
|
|
44
|
+
stdout="",
|
|
45
|
+
stderr=f"rm: unrecognized option '{arg}'\n",
|
|
46
|
+
exit_code=1,
|
|
47
|
+
)
|
|
48
|
+
elif arg.startswith("-") and arg != "-":
|
|
49
|
+
for c in arg[1:]:
|
|
50
|
+
if c == "f":
|
|
51
|
+
force = True
|
|
52
|
+
elif c in ("r", "R"):
|
|
53
|
+
recursive = True
|
|
54
|
+
elif c == "v":
|
|
55
|
+
verbose = True
|
|
56
|
+
else:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"rm: invalid option -- '{c}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
files.append(arg)
|
|
64
|
+
i += 1
|
|
65
|
+
|
|
66
|
+
if not files:
|
|
67
|
+
return ExecResult(
|
|
68
|
+
stdout="",
|
|
69
|
+
stderr="rm: missing operand\n",
|
|
70
|
+
exit_code=1,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
stdout = ""
|
|
74
|
+
stderr = ""
|
|
75
|
+
exit_code = 0
|
|
76
|
+
|
|
77
|
+
for f in files:
|
|
78
|
+
try:
|
|
79
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
80
|
+
|
|
81
|
+
# Check if it's a directory
|
|
82
|
+
try:
|
|
83
|
+
st = await ctx.fs.stat(path)
|
|
84
|
+
if st.is_directory and not recursive:
|
|
85
|
+
stderr += f"rm: cannot remove '{f}': Is a directory\n"
|
|
86
|
+
exit_code = 1
|
|
87
|
+
continue
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
if force:
|
|
90
|
+
continue
|
|
91
|
+
stderr += f"rm: cannot remove '{f}': No such file or directory\n"
|
|
92
|
+
exit_code = 1
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
await ctx.fs.rm(path, recursive=recursive, force=force)
|
|
96
|
+
if verbose:
|
|
97
|
+
stdout += f"removed '{f}'\n"
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
if not force:
|
|
100
|
+
stderr += f"rm: cannot remove '{f}': No such file or directory\n"
|
|
101
|
+
exit_code = 1
|
|
102
|
+
except OSError as e:
|
|
103
|
+
stderr += f"rm: cannot remove '{f}': {e}\n"
|
|
104
|
+
exit_code = 1
|
|
105
|
+
|
|
106
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Search engine utility module for grep and rg commands."""
|
|
2
|
+
|
|
3
|
+
from .matcher import SearchOptions, SearchResult, search_content
|
|
4
|
+
from .regex import RegexMode, build_regex, convert_replacement
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"RegexMode",
|
|
8
|
+
"build_regex",
|
|
9
|
+
"convert_replacement",
|
|
10
|
+
"SearchOptions",
|
|
11
|
+
"SearchResult",
|
|
12
|
+
"search_content",
|
|
13
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Content searching and matching utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SearchOptions:
|
|
9
|
+
"""Options for content searching."""
|
|
10
|
+
|
|
11
|
+
invert_match: bool = False
|
|
12
|
+
show_line_numbers: bool = False
|
|
13
|
+
count_only: bool = False
|
|
14
|
+
only_matching: bool = False
|
|
15
|
+
filename: str | None = None
|
|
16
|
+
before_context: int = 0
|
|
17
|
+
after_context: int = 0
|
|
18
|
+
max_count: int | None = None
|
|
19
|
+
replace: str | None = None
|
|
20
|
+
multiline: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SearchResult:
|
|
25
|
+
"""Result of a content search."""
|
|
26
|
+
|
|
27
|
+
output: str
|
|
28
|
+
matched: bool
|
|
29
|
+
match_count: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def search_content(
|
|
33
|
+
content: str,
|
|
34
|
+
regex: re.Pattern,
|
|
35
|
+
options: SearchOptions = None,
|
|
36
|
+
) -> SearchResult:
|
|
37
|
+
"""Search content and return formatted output.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
content: The text content to search
|
|
41
|
+
regex: Compiled regex pattern to search for
|
|
42
|
+
options: Search options (defaults to SearchOptions())
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
SearchResult with output, matched flag, and match count
|
|
46
|
+
"""
|
|
47
|
+
if options is None:
|
|
48
|
+
options = SearchOptions()
|
|
49
|
+
|
|
50
|
+
lines = content.split("\n")
|
|
51
|
+
# Handle trailing empty line from split
|
|
52
|
+
if lines and lines[-1] == "":
|
|
53
|
+
lines = lines[:-1]
|
|
54
|
+
|
|
55
|
+
# Track matches
|
|
56
|
+
matches: list[tuple[int, str, list[re.Match]]] = []
|
|
57
|
+
match_count = 0
|
|
58
|
+
|
|
59
|
+
# Find all matching lines
|
|
60
|
+
for line_num, line in enumerate(lines, 1):
|
|
61
|
+
line_matches = list(regex.finditer(line))
|
|
62
|
+
is_match = bool(line_matches)
|
|
63
|
+
|
|
64
|
+
if options.invert_match:
|
|
65
|
+
is_match = not is_match
|
|
66
|
+
|
|
67
|
+
if is_match:
|
|
68
|
+
if options.invert_match:
|
|
69
|
+
matches.append((line_num, line, []))
|
|
70
|
+
else:
|
|
71
|
+
matches.append((line_num, line, line_matches))
|
|
72
|
+
match_count += len(line_matches)
|
|
73
|
+
|
|
74
|
+
# Check max count
|
|
75
|
+
if options.max_count is not None and len(matches) >= options.max_count:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
if not matches:
|
|
79
|
+
return SearchResult(output="", matched=False, match_count=0)
|
|
80
|
+
|
|
81
|
+
# For count_only, just return the count info
|
|
82
|
+
if options.count_only:
|
|
83
|
+
return SearchResult(
|
|
84
|
+
output=str(len(matches)),
|
|
85
|
+
matched=True,
|
|
86
|
+
match_count=match_count,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Build output with context
|
|
90
|
+
output_lines: list[str] = []
|
|
91
|
+
|
|
92
|
+
if options.before_context > 0 or options.after_context > 0:
|
|
93
|
+
# Collect all lines to output (including context)
|
|
94
|
+
lines_to_show: dict[int, tuple[str, bool]] = {} # line_num -> (content, is_match)
|
|
95
|
+
|
|
96
|
+
for line_num, line, line_matches in matches:
|
|
97
|
+
# Add context before
|
|
98
|
+
for ctx_num in range(max(1, line_num - options.before_context), line_num):
|
|
99
|
+
if ctx_num not in lines_to_show:
|
|
100
|
+
lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
|
|
101
|
+
# Add the match line
|
|
102
|
+
lines_to_show[line_num] = (line, True)
|
|
103
|
+
# Add context after
|
|
104
|
+
end_ctx = min(len(lines) + 1, line_num + options.after_context + 1)
|
|
105
|
+
for ctx_num in range(line_num + 1, end_ctx):
|
|
106
|
+
if ctx_num not in lines_to_show:
|
|
107
|
+
lines_to_show[ctx_num] = (lines[ctx_num - 1], False)
|
|
108
|
+
|
|
109
|
+
# Output in order
|
|
110
|
+
for line_num in sorted(lines_to_show.keys()):
|
|
111
|
+
line_content, is_match = lines_to_show[line_num]
|
|
112
|
+
sep = ":" if is_match else "-"
|
|
113
|
+
|
|
114
|
+
if options.only_matching and is_match:
|
|
115
|
+
# For context lines with only_matching, we don't output them
|
|
116
|
+
# But for simplicity, include all for now
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
output_line = _format_line(
|
|
120
|
+
line_num, line_content, options, sep=sep
|
|
121
|
+
)
|
|
122
|
+
output_lines.append(output_line)
|
|
123
|
+
else:
|
|
124
|
+
# Normal output without context
|
|
125
|
+
for line_num, line, line_matches in matches:
|
|
126
|
+
if options.only_matching and line_matches:
|
|
127
|
+
# Output each match separately
|
|
128
|
+
for m in line_matches:
|
|
129
|
+
output_line = _format_line(
|
|
130
|
+
line_num, m.group(0), options
|
|
131
|
+
)
|
|
132
|
+
output_lines.append(output_line)
|
|
133
|
+
else:
|
|
134
|
+
# Handle replacement
|
|
135
|
+
output_text = line
|
|
136
|
+
if options.replace is not None and line_matches:
|
|
137
|
+
output_text = regex.sub(options.replace, line)
|
|
138
|
+
|
|
139
|
+
output_line = _format_line(line_num, output_text, options)
|
|
140
|
+
output_lines.append(output_line)
|
|
141
|
+
|
|
142
|
+
output = "\n".join(output_lines)
|
|
143
|
+
if output:
|
|
144
|
+
output += "\n"
|
|
145
|
+
|
|
146
|
+
return SearchResult(
|
|
147
|
+
output=output,
|
|
148
|
+
matched=True,
|
|
149
|
+
match_count=match_count,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _format_line(
|
|
154
|
+
line_num: int,
|
|
155
|
+
content: str,
|
|
156
|
+
options: SearchOptions,
|
|
157
|
+
sep: str = ":",
|
|
158
|
+
) -> str:
|
|
159
|
+
"""Format a single output line with optional prefix."""
|
|
160
|
+
parts = []
|
|
161
|
+
|
|
162
|
+
if options.filename:
|
|
163
|
+
parts.append(f"{options.filename}{sep}")
|
|
164
|
+
|
|
165
|
+
if options.show_line_numbers:
|
|
166
|
+
parts.append(f"{line_num}{sep}")
|
|
167
|
+
|
|
168
|
+
parts.append(content)
|
|
169
|
+
|
|
170
|
+
return "".join(parts)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Regex pattern building utilities for search commands."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RegexMode(Enum):
|
|
8
|
+
"""Regex interpretation modes."""
|
|
9
|
+
|
|
10
|
+
BASIC = "basic" # BRE - escape +?|(){}
|
|
11
|
+
EXTENDED = "extended" # ERE - standard regex
|
|
12
|
+
FIXED = "fixed" # Literal string matching
|
|
13
|
+
PERL = "perl" # PCRE (Python default)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_regex(
|
|
17
|
+
pattern: str,
|
|
18
|
+
mode: RegexMode = RegexMode.EXTENDED,
|
|
19
|
+
ignore_case: bool = False,
|
|
20
|
+
whole_word: bool = False,
|
|
21
|
+
line_regexp: bool = False,
|
|
22
|
+
multiline: bool = False,
|
|
23
|
+
) -> re.Pattern:
|
|
24
|
+
"""Build a compiled regex from pattern with options.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
pattern: The search pattern
|
|
28
|
+
mode: How to interpret the pattern (basic, extended, fixed, perl)
|
|
29
|
+
ignore_case: Case-insensitive matching
|
|
30
|
+
whole_word: Match whole words only (add word boundaries)
|
|
31
|
+
line_regexp: Match entire lines (anchor to line start/end)
|
|
32
|
+
multiline: Enable multiline mode for ^ and $
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Compiled regex pattern
|
|
36
|
+
"""
|
|
37
|
+
# Process pattern based on mode
|
|
38
|
+
if mode == RegexMode.FIXED:
|
|
39
|
+
# Escape all regex metacharacters for literal matching
|
|
40
|
+
pattern = re.escape(pattern)
|
|
41
|
+
elif mode == RegexMode.BASIC:
|
|
42
|
+
# BRE mode: +?|(){} are literal unless escaped
|
|
43
|
+
# Convert BRE to ERE by escaping these chars
|
|
44
|
+
# In BRE, \+ means one-or-more, + means literal +
|
|
45
|
+
# We need to swap the meaning
|
|
46
|
+
pattern = _convert_bre_to_ere(pattern)
|
|
47
|
+
# EXTENDED and PERL modes use pattern as-is (Python re is PCRE-like)
|
|
48
|
+
|
|
49
|
+
# Apply word boundaries
|
|
50
|
+
if whole_word:
|
|
51
|
+
pattern = r"\b(?:" + pattern + r")\b"
|
|
52
|
+
|
|
53
|
+
# Apply line anchors
|
|
54
|
+
if line_regexp:
|
|
55
|
+
pattern = "^(?:" + pattern + ")$"
|
|
56
|
+
|
|
57
|
+
# Build flags
|
|
58
|
+
flags = 0
|
|
59
|
+
if ignore_case:
|
|
60
|
+
flags |= re.IGNORECASE
|
|
61
|
+
if multiline:
|
|
62
|
+
flags |= re.MULTILINE
|
|
63
|
+
|
|
64
|
+
return re.compile(pattern, flags)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _convert_bre_to_ere(pattern: str) -> str:
|
|
68
|
+
"""Convert Basic Regular Expression to Extended Regular Expression.
|
|
69
|
+
|
|
70
|
+
In BRE, characters like +, ?, |, (, ), {, } are literal unless escaped.
|
|
71
|
+
In ERE (Python's default), they are special unless escaped.
|
|
72
|
+
|
|
73
|
+
This function escapes these characters so they're treated as literals.
|
|
74
|
+
"""
|
|
75
|
+
# Characters that are special in ERE but literal in BRE
|
|
76
|
+
literal_chars = "+?|(){}[]"
|
|
77
|
+
|
|
78
|
+
result = []
|
|
79
|
+
i = 0
|
|
80
|
+
while i < len(pattern):
|
|
81
|
+
char = pattern[i]
|
|
82
|
+
|
|
83
|
+
if char == "\\" and i + 1 < len(pattern):
|
|
84
|
+
next_char = pattern[i + 1]
|
|
85
|
+
# In BRE, \+ means one-or-more (special)
|
|
86
|
+
# So we should NOT escape it (keep as +)
|
|
87
|
+
if next_char in literal_chars:
|
|
88
|
+
result.append(next_char)
|
|
89
|
+
i += 2
|
|
90
|
+
continue
|
|
91
|
+
else:
|
|
92
|
+
# Keep other escape sequences as-is
|
|
93
|
+
result.append(char)
|
|
94
|
+
result.append(next_char)
|
|
95
|
+
i += 2
|
|
96
|
+
continue
|
|
97
|
+
elif char in literal_chars:
|
|
98
|
+
# Literal in BRE, so escape for ERE
|
|
99
|
+
result.append("\\" + char)
|
|
100
|
+
else:
|
|
101
|
+
result.append(char)
|
|
102
|
+
i += 1
|
|
103
|
+
|
|
104
|
+
return "".join(result)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_replacement(replacement: str) -> str:
|
|
108
|
+
"""Convert sed/ripgrep-style replacement to Python re.sub format.
|
|
109
|
+
|
|
110
|
+
Converts:
|
|
111
|
+
$0 -> \\g<0> (full match)
|
|
112
|
+
$1, $2, ... -> \\1, \\2, ... (numbered groups)
|
|
113
|
+
$name -> \\g<name> (named groups)
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
replacement: The replacement string with $ references
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Replacement string with Python-style backreferences
|
|
120
|
+
"""
|
|
121
|
+
result = []
|
|
122
|
+
i = 0
|
|
123
|
+
while i < len(replacement):
|
|
124
|
+
char = replacement[i]
|
|
125
|
+
|
|
126
|
+
if char == "$" and i + 1 < len(replacement):
|
|
127
|
+
next_char = replacement[i + 1]
|
|
128
|
+
|
|
129
|
+
# $0 - full match (needs \g<0> syntax)
|
|
130
|
+
if next_char == "0":
|
|
131
|
+
result.append(r"\g<0>")
|
|
132
|
+
i += 2
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# $1-9 - numbered groups
|
|
136
|
+
if next_char.isdigit():
|
|
137
|
+
# Collect all digits
|
|
138
|
+
j = i + 1
|
|
139
|
+
while j < len(replacement) and replacement[j].isdigit():
|
|
140
|
+
j += 1
|
|
141
|
+
num = replacement[i + 1:j]
|
|
142
|
+
result.append("\\" + num)
|
|
143
|
+
i = j
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# $name - named groups
|
|
147
|
+
if next_char.isalpha() or next_char == "_":
|
|
148
|
+
j = i + 1
|
|
149
|
+
while j < len(replacement) and (replacement[j].isalnum() or replacement[j] == "_"):
|
|
150
|
+
j += 1
|
|
151
|
+
name = replacement[i + 1:j]
|
|
152
|
+
result.append(r"\g<" + name + ">")
|
|
153
|
+
i = j
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
result.append(char)
|
|
157
|
+
i += 1
|
|
158
|
+
|
|
159
|
+
return "".join(result)
|