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,863 @@
|
|
|
1
|
+
"""Sed command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: sed [OPTION]... {script} [input-file]...
|
|
4
|
+
|
|
5
|
+
Stream editor for filtering and transforming text.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-n, --quiet, --silent suppress automatic printing of pattern space
|
|
9
|
+
-e script add the script to commands to be executed
|
|
10
|
+
-i, --in-place edit files in place
|
|
11
|
+
-E, -r use extended regular expressions
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
s/regexp/replacement/[flags] substitute
|
|
15
|
+
d delete pattern space
|
|
16
|
+
p print pattern space
|
|
17
|
+
a\\ text append text after line
|
|
18
|
+
i\\ text insert text before line
|
|
19
|
+
y/source/dest/ transliterate characters
|
|
20
|
+
q quit
|
|
21
|
+
|
|
22
|
+
Addresses:
|
|
23
|
+
N line number
|
|
24
|
+
$ last line
|
|
25
|
+
/regexp/ lines matching regexp
|
|
26
|
+
N,M range from line N to M
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from ...types import CommandContext, ExecResult
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SedAddress:
|
|
36
|
+
"""Represents a sed address."""
|
|
37
|
+
|
|
38
|
+
type: str # "line", "last", "regex", "range"
|
|
39
|
+
value: int | str | None = None
|
|
40
|
+
end_value: int | str | None = None
|
|
41
|
+
regex: re.Pattern | None = None
|
|
42
|
+
end_regex: re.Pattern | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SedCommand_:
|
|
47
|
+
"""A parsed sed command."""
|
|
48
|
+
|
|
49
|
+
cmd: str # s, d, p, a, i, y, q, h, H, g, G, x, n, N, etc.
|
|
50
|
+
address: SedAddress | None = None
|
|
51
|
+
pattern: re.Pattern | None = None
|
|
52
|
+
replacement: str | None = None
|
|
53
|
+
flags: str = ""
|
|
54
|
+
text: str = "" # For a, i, c commands
|
|
55
|
+
source: str = "" # For y command
|
|
56
|
+
dest: str = "" # For y command
|
|
57
|
+
label: str = "" # For b, t, T commands
|
|
58
|
+
filename: str = "" # For r, w commands
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SedCommand:
|
|
62
|
+
"""The sed command."""
|
|
63
|
+
|
|
64
|
+
name = "sed"
|
|
65
|
+
|
|
66
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
67
|
+
"""Execute the sed command."""
|
|
68
|
+
scripts: list[str] = []
|
|
69
|
+
silent = False
|
|
70
|
+
in_place = False
|
|
71
|
+
extended_regex = False
|
|
72
|
+
files: list[str] = []
|
|
73
|
+
|
|
74
|
+
# Parse arguments
|
|
75
|
+
i = 0
|
|
76
|
+
while i < len(args):
|
|
77
|
+
arg = args[i]
|
|
78
|
+
if arg == "--":
|
|
79
|
+
files.extend(args[i + 1:])
|
|
80
|
+
break
|
|
81
|
+
elif arg in ("-n", "--quiet", "--silent"):
|
|
82
|
+
silent = True
|
|
83
|
+
elif arg in ("-i", "--in-place"):
|
|
84
|
+
in_place = True
|
|
85
|
+
elif arg.startswith("-i"):
|
|
86
|
+
in_place = True
|
|
87
|
+
elif arg in ("-E", "-r", "--regexp-extended"):
|
|
88
|
+
extended_regex = True
|
|
89
|
+
elif arg == "-e":
|
|
90
|
+
if i + 1 < len(args):
|
|
91
|
+
i += 1
|
|
92
|
+
scripts.append(args[i])
|
|
93
|
+
else:
|
|
94
|
+
return ExecResult(
|
|
95
|
+
stdout="",
|
|
96
|
+
stderr="sed: option requires an argument -- 'e'\n",
|
|
97
|
+
exit_code=1,
|
|
98
|
+
)
|
|
99
|
+
elif arg == "-f":
|
|
100
|
+
if i + 1 < len(args):
|
|
101
|
+
i += 1
|
|
102
|
+
try:
|
|
103
|
+
path = ctx.fs.resolve_path(ctx.cwd, args[i])
|
|
104
|
+
content = await ctx.fs.read_file(path)
|
|
105
|
+
for line in content.split("\n"):
|
|
106
|
+
line = line.strip()
|
|
107
|
+
if line and not line.startswith("#"):
|
|
108
|
+
scripts.append(line)
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
return ExecResult(
|
|
111
|
+
stdout="",
|
|
112
|
+
stderr=f"sed: couldn't open file {args[i]}: No such file or directory\n",
|
|
113
|
+
exit_code=1,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
return ExecResult(
|
|
117
|
+
stdout="",
|
|
118
|
+
stderr="sed: option requires an argument -- 'f'\n",
|
|
119
|
+
exit_code=1,
|
|
120
|
+
)
|
|
121
|
+
elif arg.startswith("-") and len(arg) > 1 and not arg.startswith("--"):
|
|
122
|
+
# Combined short options
|
|
123
|
+
for j, c in enumerate(arg[1:], 1):
|
|
124
|
+
if c == "n":
|
|
125
|
+
silent = True
|
|
126
|
+
elif c == "i":
|
|
127
|
+
in_place = True
|
|
128
|
+
elif c == "E" or c == "r":
|
|
129
|
+
extended_regex = True
|
|
130
|
+
elif c == "e":
|
|
131
|
+
if j < len(arg) - 1:
|
|
132
|
+
scripts.append(arg[j + 1:])
|
|
133
|
+
break
|
|
134
|
+
elif i + 1 < len(args):
|
|
135
|
+
i += 1
|
|
136
|
+
scripts.append(args[i])
|
|
137
|
+
break
|
|
138
|
+
else:
|
|
139
|
+
return ExecResult(
|
|
140
|
+
stdout="",
|
|
141
|
+
stderr=f"sed: invalid option -- '{c}'\n",
|
|
142
|
+
exit_code=1,
|
|
143
|
+
)
|
|
144
|
+
elif not scripts:
|
|
145
|
+
# First non-option is the script
|
|
146
|
+
scripts.append(arg)
|
|
147
|
+
else:
|
|
148
|
+
files.append(arg)
|
|
149
|
+
i += 1
|
|
150
|
+
|
|
151
|
+
if not scripts:
|
|
152
|
+
return ExecResult(
|
|
153
|
+
stdout="",
|
|
154
|
+
stderr="sed: no script specified\n",
|
|
155
|
+
exit_code=1,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Parse scripts into commands
|
|
159
|
+
try:
|
|
160
|
+
commands = self._parse_scripts(scripts, extended_regex)
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
return ExecResult(
|
|
163
|
+
stdout="",
|
|
164
|
+
stderr=f"sed: {e}\n",
|
|
165
|
+
exit_code=1,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Default to stdin
|
|
169
|
+
if not files:
|
|
170
|
+
files = ["-"]
|
|
171
|
+
|
|
172
|
+
# Process files
|
|
173
|
+
all_output = ""
|
|
174
|
+
stderr = ""
|
|
175
|
+
|
|
176
|
+
for f in files:
|
|
177
|
+
try:
|
|
178
|
+
if f == "-":
|
|
179
|
+
content = ctx.stdin
|
|
180
|
+
else:
|
|
181
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
182
|
+
content = await ctx.fs.read_file(path)
|
|
183
|
+
|
|
184
|
+
# Determine the current file name for F command
|
|
185
|
+
current_file = f if f != "-" else ""
|
|
186
|
+
|
|
187
|
+
output, write_buffers, read_requests, r_file_pos = self._process_content(
|
|
188
|
+
content, commands, silent, ctx, current_file
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Process read requests (r command)
|
|
192
|
+
for idx, filename in read_requests:
|
|
193
|
+
placeholder = f"__READ_FILE__{idx}__"
|
|
194
|
+
try:
|
|
195
|
+
read_path = ctx.fs.resolve_path(ctx.cwd, filename)
|
|
196
|
+
file_content = await ctx.fs.read_file(read_path)
|
|
197
|
+
# Ensure content ends with newline
|
|
198
|
+
if file_content and not file_content.endswith("\n"):
|
|
199
|
+
file_content += "\n"
|
|
200
|
+
output = output.replace(placeholder, file_content)
|
|
201
|
+
except FileNotFoundError:
|
|
202
|
+
# Real sed silently ignores nonexistent files for r command
|
|
203
|
+
output = output.replace(placeholder, "")
|
|
204
|
+
|
|
205
|
+
# Process R command (read single line)
|
|
206
|
+
# Cache file lines for efficiency
|
|
207
|
+
r_file_cache: dict[str, list[str]] = {}
|
|
208
|
+
for filename, max_pos in r_file_pos.items():
|
|
209
|
+
if filename not in r_file_cache:
|
|
210
|
+
try:
|
|
211
|
+
read_path = ctx.fs.resolve_path(ctx.cwd, filename)
|
|
212
|
+
file_content = await ctx.fs.read_file(read_path)
|
|
213
|
+
r_file_cache[filename] = file_content.split("\n")
|
|
214
|
+
# Remove trailing empty line if file ended with newline
|
|
215
|
+
if r_file_cache[filename] and r_file_cache[filename][-1] == "":
|
|
216
|
+
r_file_cache[filename] = r_file_cache[filename][:-1]
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
r_file_cache[filename] = []
|
|
219
|
+
|
|
220
|
+
# Replace placeholders with actual lines
|
|
221
|
+
for pos in range(max_pos):
|
|
222
|
+
placeholder = f"__READ_LINE__{filename}__{pos}__"
|
|
223
|
+
if pos < len(r_file_cache[filename]):
|
|
224
|
+
line = r_file_cache[filename][pos] + "\n"
|
|
225
|
+
output = output.replace(placeholder, line)
|
|
226
|
+
else:
|
|
227
|
+
# No more lines in file
|
|
228
|
+
output = output.replace(placeholder, "")
|
|
229
|
+
|
|
230
|
+
# Process write buffers (w command)
|
|
231
|
+
for filename, lines in write_buffers.items():
|
|
232
|
+
try:
|
|
233
|
+
write_path = ctx.fs.resolve_path(ctx.cwd, filename)
|
|
234
|
+
write_content = "\n".join(lines)
|
|
235
|
+
if write_content and not write_content.endswith("\n"):
|
|
236
|
+
write_content += "\n"
|
|
237
|
+
await ctx.fs.write_file(write_path, write_content)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass # Silently ignore write errors like real sed
|
|
240
|
+
|
|
241
|
+
if in_place and f != "-":
|
|
242
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
243
|
+
await ctx.fs.write_file(path, output)
|
|
244
|
+
else:
|
|
245
|
+
all_output += output
|
|
246
|
+
|
|
247
|
+
except FileNotFoundError:
|
|
248
|
+
stderr += f"sed: {f}: No such file or directory\n"
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
if stderr:
|
|
252
|
+
return ExecResult(stdout=all_output, stderr=stderr, exit_code=1)
|
|
253
|
+
|
|
254
|
+
return ExecResult(stdout=all_output, stderr="", exit_code=0)
|
|
255
|
+
|
|
256
|
+
def _parse_scripts(self, scripts: list[str], extended_regex: bool) -> list[SedCommand_]:
|
|
257
|
+
"""Parse sed scripts into commands."""
|
|
258
|
+
commands: list[SedCommand_] = []
|
|
259
|
+
|
|
260
|
+
for script in scripts:
|
|
261
|
+
# Handle multiple commands separated by semicolons or newlines
|
|
262
|
+
for cmd_str in re.split(r"[;\n]", script):
|
|
263
|
+
cmd_str = cmd_str.strip()
|
|
264
|
+
if not cmd_str:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
cmd = self._parse_command(cmd_str, extended_regex)
|
|
268
|
+
if cmd:
|
|
269
|
+
commands.append(cmd)
|
|
270
|
+
|
|
271
|
+
return commands
|
|
272
|
+
|
|
273
|
+
def _parse_command(self, cmd_str: str, extended_regex: bool) -> SedCommand_ | None:
|
|
274
|
+
"""Parse a single sed command."""
|
|
275
|
+
pos = 0
|
|
276
|
+
address = None
|
|
277
|
+
|
|
278
|
+
# Parse address if present
|
|
279
|
+
if cmd_str and cmd_str[0].isdigit():
|
|
280
|
+
# Line number address
|
|
281
|
+
match = re.match(r"(\d+)(?:,(\d+|\$))?", cmd_str)
|
|
282
|
+
if match:
|
|
283
|
+
start = int(match.group(1))
|
|
284
|
+
if match.group(2):
|
|
285
|
+
if match.group(2) == "$":
|
|
286
|
+
address = SedAddress(type="range", value=start, end_value="$")
|
|
287
|
+
else:
|
|
288
|
+
address = SedAddress(type="range", value=start, end_value=int(match.group(2)))
|
|
289
|
+
else:
|
|
290
|
+
address = SedAddress(type="line", value=start)
|
|
291
|
+
pos = match.end()
|
|
292
|
+
elif cmd_str and cmd_str[0] == "$":
|
|
293
|
+
address = SedAddress(type="last")
|
|
294
|
+
pos = 1
|
|
295
|
+
elif cmd_str and cmd_str[0] == "/":
|
|
296
|
+
# Regex address
|
|
297
|
+
end = self._find_delimiter(cmd_str, 1, "/")
|
|
298
|
+
if end != -1:
|
|
299
|
+
pattern = cmd_str[1:end]
|
|
300
|
+
flags = re.IGNORECASE if extended_regex else 0
|
|
301
|
+
try:
|
|
302
|
+
address = SedAddress(type="regex", regex=re.compile(pattern, flags))
|
|
303
|
+
except re.error as e:
|
|
304
|
+
raise ValueError(f"invalid regex: {e}")
|
|
305
|
+
pos = end + 1
|
|
306
|
+
|
|
307
|
+
# Check for range
|
|
308
|
+
if pos < len(cmd_str) and cmd_str[pos] == ",":
|
|
309
|
+
pos += 1
|
|
310
|
+
if pos < len(cmd_str):
|
|
311
|
+
if cmd_str[pos] == "$":
|
|
312
|
+
address = SedAddress(type="range", regex=address.regex, end_value="$")
|
|
313
|
+
pos += 1
|
|
314
|
+
elif cmd_str[pos].isdigit():
|
|
315
|
+
match = re.match(r"(\d+)", cmd_str[pos:])
|
|
316
|
+
if match:
|
|
317
|
+
address = SedAddress(type="range", regex=address.regex, end_value=int(match.group(1)))
|
|
318
|
+
pos += match.end()
|
|
319
|
+
elif cmd_str[pos] == "/":
|
|
320
|
+
end2 = self._find_delimiter(cmd_str, pos + 1, "/")
|
|
321
|
+
if end2 != -1:
|
|
322
|
+
pattern2 = cmd_str[pos + 1:end2]
|
|
323
|
+
try:
|
|
324
|
+
address = SedAddress(
|
|
325
|
+
type="range",
|
|
326
|
+
regex=address.regex,
|
|
327
|
+
end_regex=re.compile(pattern2, flags),
|
|
328
|
+
)
|
|
329
|
+
except re.error as e:
|
|
330
|
+
raise ValueError(f"invalid regex: {e}")
|
|
331
|
+
pos = end2 + 1
|
|
332
|
+
|
|
333
|
+
# Skip whitespace
|
|
334
|
+
while pos < len(cmd_str) and cmd_str[pos] in " \t":
|
|
335
|
+
pos += 1
|
|
336
|
+
|
|
337
|
+
if pos >= len(cmd_str):
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
cmd_char = cmd_str[pos]
|
|
341
|
+
pos += 1
|
|
342
|
+
|
|
343
|
+
if cmd_char == "s":
|
|
344
|
+
# Substitution
|
|
345
|
+
if pos >= len(cmd_str):
|
|
346
|
+
raise ValueError("unterminated s command")
|
|
347
|
+
|
|
348
|
+
delim = cmd_str[pos]
|
|
349
|
+
pos += 1
|
|
350
|
+
|
|
351
|
+
# Find pattern
|
|
352
|
+
end = self._find_delimiter(cmd_str, pos, delim)
|
|
353
|
+
if end == -1:
|
|
354
|
+
raise ValueError("unterminated s command")
|
|
355
|
+
pattern = cmd_str[pos:end]
|
|
356
|
+
pos = end + 1
|
|
357
|
+
|
|
358
|
+
# Find replacement
|
|
359
|
+
end = self._find_delimiter(cmd_str, pos, delim)
|
|
360
|
+
if end == -1:
|
|
361
|
+
raise ValueError("unterminated s command")
|
|
362
|
+
replacement = cmd_str[pos:end]
|
|
363
|
+
pos = end + 1
|
|
364
|
+
|
|
365
|
+
# Parse flags
|
|
366
|
+
flags = cmd_str[pos:] if pos < len(cmd_str) else ""
|
|
367
|
+
|
|
368
|
+
regex_flags = 0
|
|
369
|
+
if "i" in flags or extended_regex:
|
|
370
|
+
regex_flags |= re.IGNORECASE
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
compiled = re.compile(pattern, regex_flags)
|
|
374
|
+
except re.error as e:
|
|
375
|
+
raise ValueError(f"invalid regex: {e}")
|
|
376
|
+
|
|
377
|
+
return SedCommand_(
|
|
378
|
+
cmd="s",
|
|
379
|
+
address=address,
|
|
380
|
+
pattern=compiled,
|
|
381
|
+
replacement=replacement,
|
|
382
|
+
flags=flags,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
elif cmd_char == "y":
|
|
386
|
+
# Transliterate
|
|
387
|
+
if pos >= len(cmd_str):
|
|
388
|
+
raise ValueError("unterminated y command")
|
|
389
|
+
|
|
390
|
+
delim = cmd_str[pos]
|
|
391
|
+
pos += 1
|
|
392
|
+
|
|
393
|
+
end = self._find_delimiter(cmd_str, pos, delim)
|
|
394
|
+
if end == -1:
|
|
395
|
+
raise ValueError("unterminated y command")
|
|
396
|
+
source = cmd_str[pos:end]
|
|
397
|
+
pos = end + 1
|
|
398
|
+
|
|
399
|
+
end = self._find_delimiter(cmd_str, pos, delim)
|
|
400
|
+
if end == -1:
|
|
401
|
+
raise ValueError("unterminated y command")
|
|
402
|
+
dest = cmd_str[pos:end]
|
|
403
|
+
|
|
404
|
+
if len(source) != len(dest):
|
|
405
|
+
raise ValueError("y command requires equal length strings")
|
|
406
|
+
|
|
407
|
+
return SedCommand_(
|
|
408
|
+
cmd="y",
|
|
409
|
+
address=address,
|
|
410
|
+
source=source,
|
|
411
|
+
dest=dest,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
elif cmd_char == "d":
|
|
415
|
+
return SedCommand_(cmd="d", address=address)
|
|
416
|
+
|
|
417
|
+
elif cmd_char == "=":
|
|
418
|
+
# Print line number
|
|
419
|
+
return SedCommand_(cmd="=", address=address)
|
|
420
|
+
|
|
421
|
+
elif cmd_char == "p":
|
|
422
|
+
return SedCommand_(cmd="p", address=address)
|
|
423
|
+
|
|
424
|
+
elif cmd_char == "l":
|
|
425
|
+
# List pattern space with escapes
|
|
426
|
+
return SedCommand_(cmd="l", address=address)
|
|
427
|
+
|
|
428
|
+
elif cmd_char == "F":
|
|
429
|
+
# Print filename
|
|
430
|
+
return SedCommand_(cmd="F", address=address)
|
|
431
|
+
|
|
432
|
+
elif cmd_char == "q":
|
|
433
|
+
return SedCommand_(cmd="q", address=address)
|
|
434
|
+
|
|
435
|
+
elif cmd_char in ("a", "i", "c"):
|
|
436
|
+
# Append, insert, or change
|
|
437
|
+
text = cmd_str[pos:].lstrip()
|
|
438
|
+
if text.startswith("\\"):
|
|
439
|
+
text = text[1:].lstrip()
|
|
440
|
+
return SedCommand_(cmd=cmd_char, address=address, text=text)
|
|
441
|
+
|
|
442
|
+
elif cmd_char in ("h", "H", "g", "G", "x"):
|
|
443
|
+
# Hold space commands
|
|
444
|
+
return SedCommand_(cmd=cmd_char, address=address)
|
|
445
|
+
|
|
446
|
+
elif cmd_char in ("n", "N"):
|
|
447
|
+
# Next line commands
|
|
448
|
+
return SedCommand_(cmd=cmd_char, address=address)
|
|
449
|
+
|
|
450
|
+
elif cmd_char in ("P", "D"):
|
|
451
|
+
# Print/delete first line of pattern space
|
|
452
|
+
return SedCommand_(cmd=cmd_char, address=address)
|
|
453
|
+
|
|
454
|
+
elif cmd_char == "b":
|
|
455
|
+
# Branch to label
|
|
456
|
+
label = cmd_str[pos:].strip()
|
|
457
|
+
return SedCommand_(cmd="b", address=address, label=label)
|
|
458
|
+
|
|
459
|
+
elif cmd_char == "t":
|
|
460
|
+
# Branch on successful substitute
|
|
461
|
+
label = cmd_str[pos:].strip()
|
|
462
|
+
return SedCommand_(cmd="t", address=address, label=label)
|
|
463
|
+
|
|
464
|
+
elif cmd_char == "T":
|
|
465
|
+
# Branch on failed substitute
|
|
466
|
+
label = cmd_str[pos:].strip()
|
|
467
|
+
return SedCommand_(cmd="T", address=address, label=label)
|
|
468
|
+
|
|
469
|
+
elif cmd_char == ":":
|
|
470
|
+
# Label definition
|
|
471
|
+
label = cmd_str[pos:].strip()
|
|
472
|
+
return SedCommand_(cmd=":", label=label)
|
|
473
|
+
|
|
474
|
+
elif cmd_char == "r":
|
|
475
|
+
# Read file
|
|
476
|
+
filename = cmd_str[pos:].strip()
|
|
477
|
+
return SedCommand_(cmd="r", address=address, filename=filename)
|
|
478
|
+
|
|
479
|
+
elif cmd_char == "w":
|
|
480
|
+
# Write to file
|
|
481
|
+
filename = cmd_str[pos:].strip()
|
|
482
|
+
return SedCommand_(cmd="w", address=address, filename=filename)
|
|
483
|
+
|
|
484
|
+
elif cmd_char == "R":
|
|
485
|
+
# Read single line from file
|
|
486
|
+
filename = cmd_str[pos:].strip()
|
|
487
|
+
return SedCommand_(cmd="R", address=address, filename=filename)
|
|
488
|
+
|
|
489
|
+
elif cmd_char == "{":
|
|
490
|
+
# Start of block (handled in parsing)
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
elif cmd_char == "}":
|
|
494
|
+
# End of block (handled in parsing)
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
else:
|
|
498
|
+
raise ValueError(f"unknown command: {cmd_char}")
|
|
499
|
+
|
|
500
|
+
def _find_delimiter(self, s: str, start: int, delim: str) -> int:
|
|
501
|
+
"""Find the next unescaped delimiter."""
|
|
502
|
+
i = start
|
|
503
|
+
while i < len(s):
|
|
504
|
+
if s[i] == "\\" and i + 1 < len(s):
|
|
505
|
+
i += 2
|
|
506
|
+
elif s[i] == delim:
|
|
507
|
+
return i
|
|
508
|
+
else:
|
|
509
|
+
i += 1
|
|
510
|
+
return -1
|
|
511
|
+
|
|
512
|
+
def _process_content(
|
|
513
|
+
self, content: str, commands: list[SedCommand_], silent: bool,
|
|
514
|
+
ctx: "CommandContext | None" = None,
|
|
515
|
+
current_file: str = ""
|
|
516
|
+
) -> tuple[str, dict[str, list[str]], list[tuple[int, str]], dict[str, int]]:
|
|
517
|
+
"""Process content through sed commands."""
|
|
518
|
+
lines = content.split("\n")
|
|
519
|
+
# Remove trailing empty line if present
|
|
520
|
+
if lines and lines[-1] == "":
|
|
521
|
+
lines = lines[:-1]
|
|
522
|
+
|
|
523
|
+
output = ""
|
|
524
|
+
total_lines = len(lines)
|
|
525
|
+
|
|
526
|
+
# Track range state for each command
|
|
527
|
+
in_range: dict[int, bool] = {}
|
|
528
|
+
|
|
529
|
+
# Build label index
|
|
530
|
+
labels: dict[str, int] = {}
|
|
531
|
+
for cmd_idx, cmd in enumerate(commands):
|
|
532
|
+
if cmd.cmd == ":" and cmd.label:
|
|
533
|
+
labels[cmd.label] = cmd_idx
|
|
534
|
+
|
|
535
|
+
# Hold space
|
|
536
|
+
hold_space = ""
|
|
537
|
+
|
|
538
|
+
# Track if last substitute succeeded (for t/T branching)
|
|
539
|
+
sub_succeeded = False
|
|
540
|
+
|
|
541
|
+
# Write file buffers
|
|
542
|
+
write_buffers: dict[str, list[str]] = {}
|
|
543
|
+
# Read file requests: (output_position, filename)
|
|
544
|
+
read_requests: list[tuple[int, str]] = []
|
|
545
|
+
# R command file state: tracks current line position for each file
|
|
546
|
+
r_file_lines: dict[str, list[str]] = {}
|
|
547
|
+
r_file_pos: dict[str, int] = {}
|
|
548
|
+
|
|
549
|
+
line_idx = 0
|
|
550
|
+
while line_idx < len(lines):
|
|
551
|
+
line = lines[line_idx]
|
|
552
|
+
line_num = line_idx + 1
|
|
553
|
+
pattern_space = line
|
|
554
|
+
deleted = False
|
|
555
|
+
insert_text = ""
|
|
556
|
+
append_text = ""
|
|
557
|
+
read_text = ""
|
|
558
|
+
should_quit = False
|
|
559
|
+
restart_cycle = False
|
|
560
|
+
|
|
561
|
+
cmd_idx = 0
|
|
562
|
+
while cmd_idx < len(commands):
|
|
563
|
+
cmd = commands[cmd_idx]
|
|
564
|
+
|
|
565
|
+
if deleted or should_quit:
|
|
566
|
+
break
|
|
567
|
+
|
|
568
|
+
# Skip label definitions
|
|
569
|
+
if cmd.cmd == ":":
|
|
570
|
+
cmd_idx += 1
|
|
571
|
+
continue
|
|
572
|
+
|
|
573
|
+
# Check if address matches
|
|
574
|
+
if not self._address_matches(cmd.address, line_num, total_lines, pattern_space, in_range, cmd_idx):
|
|
575
|
+
cmd_idx += 1
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
if cmd.cmd == "s":
|
|
579
|
+
# Substitution
|
|
580
|
+
if cmd.pattern and cmd.replacement is not None:
|
|
581
|
+
if "g" in cmd.flags:
|
|
582
|
+
new_pattern = cmd.pattern.sub(
|
|
583
|
+
self._expand_replacement(cmd.replacement), pattern_space
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
new_pattern = cmd.pattern.sub(
|
|
587
|
+
self._expand_replacement(cmd.replacement), pattern_space, count=1
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if new_pattern != pattern_space:
|
|
591
|
+
pattern_space = new_pattern
|
|
592
|
+
sub_succeeded = True
|
|
593
|
+
if "p" in cmd.flags:
|
|
594
|
+
output += pattern_space + "\n"
|
|
595
|
+
else:
|
|
596
|
+
sub_succeeded = False
|
|
597
|
+
|
|
598
|
+
elif cmd.cmd == "y":
|
|
599
|
+
# Transliterate
|
|
600
|
+
trans = str.maketrans(cmd.source, cmd.dest)
|
|
601
|
+
pattern_space = pattern_space.translate(trans)
|
|
602
|
+
|
|
603
|
+
elif cmd.cmd == "d":
|
|
604
|
+
deleted = True
|
|
605
|
+
|
|
606
|
+
elif cmd.cmd == "D":
|
|
607
|
+
# Delete first line of pattern space
|
|
608
|
+
if "\n" in pattern_space:
|
|
609
|
+
pattern_space = pattern_space.split("\n", 1)[1]
|
|
610
|
+
# Restart with remaining pattern space
|
|
611
|
+
cmd_idx = 0
|
|
612
|
+
continue
|
|
613
|
+
else:
|
|
614
|
+
deleted = True
|
|
615
|
+
|
|
616
|
+
elif cmd.cmd == "=":
|
|
617
|
+
# Print line number
|
|
618
|
+
output += str(line_num) + "\n"
|
|
619
|
+
|
|
620
|
+
elif cmd.cmd == "p":
|
|
621
|
+
output += pattern_space + "\n"
|
|
622
|
+
|
|
623
|
+
elif cmd.cmd == "l":
|
|
624
|
+
# List pattern space with escapes
|
|
625
|
+
escaped = self._escape_for_list(pattern_space)
|
|
626
|
+
output += escaped + "$\n"
|
|
627
|
+
|
|
628
|
+
elif cmd.cmd == "F":
|
|
629
|
+
# Print current filename
|
|
630
|
+
output += current_file + "\n"
|
|
631
|
+
|
|
632
|
+
elif cmd.cmd == "P":
|
|
633
|
+
# Print first line of pattern space
|
|
634
|
+
first_line = pattern_space.split("\n", 1)[0]
|
|
635
|
+
output += first_line + "\n"
|
|
636
|
+
|
|
637
|
+
elif cmd.cmd == "a":
|
|
638
|
+
append_text += cmd.text + "\n"
|
|
639
|
+
|
|
640
|
+
elif cmd.cmd == "i":
|
|
641
|
+
insert_text += cmd.text + "\n"
|
|
642
|
+
|
|
643
|
+
elif cmd.cmd == "c":
|
|
644
|
+
# Change: replace pattern space and delete
|
|
645
|
+
output += cmd.text + "\n"
|
|
646
|
+
deleted = True
|
|
647
|
+
|
|
648
|
+
elif cmd.cmd == "q":
|
|
649
|
+
should_quit = True
|
|
650
|
+
|
|
651
|
+
elif cmd.cmd == "h":
|
|
652
|
+
# Copy pattern space to hold space
|
|
653
|
+
hold_space = pattern_space
|
|
654
|
+
|
|
655
|
+
elif cmd.cmd == "H":
|
|
656
|
+
# Append pattern space to hold space
|
|
657
|
+
if hold_space:
|
|
658
|
+
hold_space += "\n" + pattern_space
|
|
659
|
+
else:
|
|
660
|
+
hold_space = pattern_space
|
|
661
|
+
|
|
662
|
+
elif cmd.cmd == "g":
|
|
663
|
+
# Copy hold space to pattern space
|
|
664
|
+
pattern_space = hold_space
|
|
665
|
+
|
|
666
|
+
elif cmd.cmd == "G":
|
|
667
|
+
# Append hold space to pattern space
|
|
668
|
+
pattern_space += "\n" + hold_space
|
|
669
|
+
|
|
670
|
+
elif cmd.cmd == "x":
|
|
671
|
+
# Exchange pattern and hold space
|
|
672
|
+
pattern_space, hold_space = hold_space, pattern_space
|
|
673
|
+
|
|
674
|
+
elif cmd.cmd == "n":
|
|
675
|
+
# Print pattern space (unless silent), read next line
|
|
676
|
+
if not silent:
|
|
677
|
+
output += pattern_space + "\n"
|
|
678
|
+
line_idx += 1
|
|
679
|
+
if line_idx < len(lines):
|
|
680
|
+
pattern_space = lines[line_idx]
|
|
681
|
+
line_num = line_idx + 1
|
|
682
|
+
else:
|
|
683
|
+
deleted = True
|
|
684
|
+
|
|
685
|
+
elif cmd.cmd == "N":
|
|
686
|
+
# Append next line to pattern space
|
|
687
|
+
line_idx += 1
|
|
688
|
+
if line_idx < len(lines):
|
|
689
|
+
pattern_space += "\n" + lines[line_idx]
|
|
690
|
+
else:
|
|
691
|
+
# No more lines, end
|
|
692
|
+
should_quit = True
|
|
693
|
+
|
|
694
|
+
elif cmd.cmd == "b":
|
|
695
|
+
# Branch to label (or end if no label)
|
|
696
|
+
if cmd.label and cmd.label in labels:
|
|
697
|
+
cmd_idx = labels[cmd.label]
|
|
698
|
+
continue
|
|
699
|
+
else:
|
|
700
|
+
# Branch to end of script
|
|
701
|
+
break
|
|
702
|
+
|
|
703
|
+
elif cmd.cmd == "t":
|
|
704
|
+
# Branch if last substitute succeeded
|
|
705
|
+
if sub_succeeded:
|
|
706
|
+
sub_succeeded = False
|
|
707
|
+
if cmd.label and cmd.label in labels:
|
|
708
|
+
cmd_idx = labels[cmd.label]
|
|
709
|
+
continue
|
|
710
|
+
else:
|
|
711
|
+
break
|
|
712
|
+
|
|
713
|
+
elif cmd.cmd == "T":
|
|
714
|
+
# Branch if last substitute failed
|
|
715
|
+
if not sub_succeeded:
|
|
716
|
+
if cmd.label and cmd.label in labels:
|
|
717
|
+
cmd_idx = labels[cmd.label]
|
|
718
|
+
continue
|
|
719
|
+
else:
|
|
720
|
+
break
|
|
721
|
+
|
|
722
|
+
elif cmd.cmd == "r":
|
|
723
|
+
# Read file (content appended after current line)
|
|
724
|
+
if cmd.filename:
|
|
725
|
+
# Store placeholder for where to insert file content
|
|
726
|
+
append_text += f"__READ_FILE__{len(read_requests)}__"
|
|
727
|
+
read_requests.append((len(read_requests), cmd.filename))
|
|
728
|
+
|
|
729
|
+
elif cmd.cmd == "w":
|
|
730
|
+
# Write pattern space to file
|
|
731
|
+
if cmd.filename:
|
|
732
|
+
if cmd.filename not in write_buffers:
|
|
733
|
+
write_buffers[cmd.filename] = []
|
|
734
|
+
write_buffers[cmd.filename].append(pattern_space)
|
|
735
|
+
|
|
736
|
+
elif cmd.cmd == "R":
|
|
737
|
+
# Read single line from file
|
|
738
|
+
if cmd.filename:
|
|
739
|
+
# Initialize file lines if not already loaded
|
|
740
|
+
if cmd.filename not in r_file_lines:
|
|
741
|
+
r_file_lines[cmd.filename] = None # Placeholder for async load
|
|
742
|
+
r_file_pos[cmd.filename] = 0
|
|
743
|
+
# Store placeholder for where to insert the line
|
|
744
|
+
pos = r_file_pos.get(cmd.filename, 0)
|
|
745
|
+
append_text += f"__READ_LINE__{cmd.filename}__{pos}__"
|
|
746
|
+
r_file_pos[cmd.filename] = pos + 1
|
|
747
|
+
|
|
748
|
+
cmd_idx += 1
|
|
749
|
+
|
|
750
|
+
# Output insert text before line
|
|
751
|
+
if insert_text:
|
|
752
|
+
output += insert_text
|
|
753
|
+
|
|
754
|
+
# Output line unless deleted or silent mode
|
|
755
|
+
if not deleted:
|
|
756
|
+
if not silent:
|
|
757
|
+
output += pattern_space + "\n"
|
|
758
|
+
|
|
759
|
+
# Output append text after line
|
|
760
|
+
if append_text:
|
|
761
|
+
output += append_text
|
|
762
|
+
|
|
763
|
+
if should_quit:
|
|
764
|
+
break
|
|
765
|
+
|
|
766
|
+
line_idx += 1
|
|
767
|
+
|
|
768
|
+
return output, write_buffers, read_requests, r_file_pos
|
|
769
|
+
|
|
770
|
+
def _address_matches(
|
|
771
|
+
self,
|
|
772
|
+
address: SedAddress | None,
|
|
773
|
+
line_num: int,
|
|
774
|
+
total_lines: int,
|
|
775
|
+
pattern_space: str,
|
|
776
|
+
in_range: dict[int, bool],
|
|
777
|
+
cmd_idx: int,
|
|
778
|
+
) -> bool:
|
|
779
|
+
"""Check if an address matches the current line."""
|
|
780
|
+
if address is None:
|
|
781
|
+
return True
|
|
782
|
+
|
|
783
|
+
if address.type == "line":
|
|
784
|
+
return line_num == address.value
|
|
785
|
+
|
|
786
|
+
elif address.type == "last":
|
|
787
|
+
return line_num == total_lines
|
|
788
|
+
|
|
789
|
+
elif address.type == "regex":
|
|
790
|
+
if address.regex:
|
|
791
|
+
return bool(address.regex.search(pattern_space))
|
|
792
|
+
return False
|
|
793
|
+
|
|
794
|
+
elif address.type == "range":
|
|
795
|
+
# Check if we're entering or in a range
|
|
796
|
+
if cmd_idx not in in_range:
|
|
797
|
+
in_range[cmd_idx] = False
|
|
798
|
+
|
|
799
|
+
if not in_range[cmd_idx]:
|
|
800
|
+
# Check start condition
|
|
801
|
+
start_match = False
|
|
802
|
+
if address.value is not None:
|
|
803
|
+
start_match = line_num == address.value
|
|
804
|
+
elif address.regex:
|
|
805
|
+
start_match = bool(address.regex.search(pattern_space))
|
|
806
|
+
|
|
807
|
+
if start_match:
|
|
808
|
+
in_range[cmd_idx] = True
|
|
809
|
+
|
|
810
|
+
if in_range[cmd_idx]:
|
|
811
|
+
# Check end condition
|
|
812
|
+
end_match = False
|
|
813
|
+
if address.end_value == "$":
|
|
814
|
+
end_match = line_num == total_lines
|
|
815
|
+
elif isinstance(address.end_value, int):
|
|
816
|
+
end_match = line_num >= address.end_value
|
|
817
|
+
elif address.end_regex:
|
|
818
|
+
end_match = bool(address.end_regex.search(pattern_space))
|
|
819
|
+
|
|
820
|
+
if end_match:
|
|
821
|
+
in_range[cmd_idx] = False
|
|
822
|
+
|
|
823
|
+
return True
|
|
824
|
+
|
|
825
|
+
return False
|
|
826
|
+
|
|
827
|
+
return False
|
|
828
|
+
|
|
829
|
+
def _expand_replacement(self, replacement: str) -> str:
|
|
830
|
+
"""Expand replacement string, handling backreferences."""
|
|
831
|
+
# Convert \1, \2, etc. to Python's \g<1>, \g<2>
|
|
832
|
+
result = replacement
|
|
833
|
+
result = re.sub(r"\\(\d)", r"\\g<\1>", result)
|
|
834
|
+
# Handle & for entire match
|
|
835
|
+
result = result.replace("&", r"\g<0>")
|
|
836
|
+
return result
|
|
837
|
+
|
|
838
|
+
def _escape_for_list(self, s: str) -> str:
|
|
839
|
+
"""Escape a string for the 'l' command output."""
|
|
840
|
+
result = []
|
|
841
|
+
for c in s:
|
|
842
|
+
if c == "\\":
|
|
843
|
+
result.append("\\\\")
|
|
844
|
+
elif c == "\t":
|
|
845
|
+
result.append("\\t")
|
|
846
|
+
elif c == "\n":
|
|
847
|
+
result.append("\\n")
|
|
848
|
+
elif c == "\r":
|
|
849
|
+
result.append("\\r")
|
|
850
|
+
elif c == "\a":
|
|
851
|
+
result.append("\\a")
|
|
852
|
+
elif c == "\b":
|
|
853
|
+
result.append("\\b")
|
|
854
|
+
elif c == "\f":
|
|
855
|
+
result.append("\\f")
|
|
856
|
+
elif c == "\v":
|
|
857
|
+
result.append("\\v")
|
|
858
|
+
elif ord(c) < 32 or ord(c) > 126:
|
|
859
|
+
# Non-printable character - show as octal or hex
|
|
860
|
+
result.append(f"\\x{ord(c):02x}")
|
|
861
|
+
else:
|
|
862
|
+
result.append(c)
|
|
863
|
+
return "".join(result)
|