align-agent 0.4.3__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.
- align_agent/__init__.py +11 -0
- align_agent/_audit/common.py +58 -0
- align_agent/_audit/deepswe.py +260 -0
- align_agent/_audit/excerpts.py +203 -0
- align_agent/_audit/extract.py +798 -0
- align_agent/_audit/materialize.py +624 -0
- align_agent/_audit/score.py +325 -0
- align_agent/_audit/shellparse.py +1141 -0
- align_agent/_audit/tarsur.py +153 -0
- align_agent/_vendor/src/__init__.py +3 -0
- align_agent/_vendor/src/harness/__init__.py +13 -0
- align_agent/_vendor/src/harness/arms.py +524 -0
- align_agent/_vendor/src/harness/completion.py +104 -0
- align_agent/_vendor/src/harness/dependency_notice.py +616 -0
- align_agent/_vendor/src/harness/freshness_hook.py +131 -0
- align_agent/_vendor/src/harness/lean.py +41 -0
- align_agent/_vendor/src/harness/mini_real.py +1040 -0
- align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
- align_agent/_vendor/src/harness/tail_diff.py +292 -0
- align_agent/_vendor/src/harness/viewport.py +220 -0
- align_agent/_vendor/src/metrics/__init__.py +38 -0
- align_agent/_vendor/src/metrics/coherence.py +354 -0
- align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
- align_agent/_vendor/src/metrics/replay.py +399 -0
- align_agent/_vendor/src/metrics/statistics.py +185 -0
- align_agent/_vendor/src/metrics/verification.py +104 -0
- align_agent/_vendor/src/parsers/__init__.py +33 -0
- align_agent/_vendor/src/parsers/base_parser.py +349 -0
- align_agent/_vendor/src/parsers/file_events.py +806 -0
- align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
- align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
- align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
- align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
- align_agent/cli.py +101 -0
- align_agent/core.py +161 -0
- align_agent/run.py +84 -0
- align_agent/sessions.py +985 -0
- align_agent/writethrough/__init__.py +4 -0
- align_agent/writethrough/_lib/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
- align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
- align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
- align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
- align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
- align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
- align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
- align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
- align_agent/writethrough/claude_code.py +81 -0
- align_agent/writethrough/engine.py +647 -0
- align_agent/writethrough/hooks.py +227 -0
- align_agent/writethrough/mcp_server.py +125 -0
- align_agent/writethrough/protocol.py +68 -0
- align_agent-0.4.3.dist-info/METADATA +224 -0
- align_agent-0.4.3.dist-info/RECORD +60 -0
- align_agent-0.4.3.dist-info/WHEEL +5 -0
- align_agent-0.4.3.dist-info/entry_points.txt +6 -0
- align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
- align_agent-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
"""Independent shell-command file-operation extractor for the RQ1 audit. Usage: imported by scripts/audit/extract.py (python3 scripts/audit/shellparse.py runs self-tests).
|
|
2
|
+
|
|
3
|
+
Written from scratch for the audit; it does not import src/parsers. Given one
|
|
4
|
+
shell command string and the working directory it starts in, it returns the
|
|
5
|
+
file operations the command performs, in order, each with:
|
|
6
|
+
|
|
7
|
+
op "R" (content reaches the agent) or "W" (file changed)
|
|
8
|
+
path absolute path after cwd resolution (canonicalised later)
|
|
9
|
+
kind R: "full" | "range" | "lines" | "listing"
|
|
10
|
+
W: "anchored" | "overwrite" | "replace" | "append" | "delete" |
|
|
11
|
+
"restore" | "touch"
|
|
12
|
+
mech the utility (sed, perl, python, patch, cp, redirect, ...)
|
|
13
|
+
anchor for anchored writes: what locates the edit (see below)
|
|
14
|
+
view for reads: {"range": [a, b]} when the shown lines are known
|
|
15
|
+
cmd_i index of the simple command inside the string
|
|
16
|
+
guard the connector before this simple command ("&&", "||", ";", "|")
|
|
17
|
+
|
|
18
|
+
Anchors:
|
|
19
|
+
{"type": "sed", "cmds": [...]} parsed sed script (s///, d, i, a, c with addresses)
|
|
20
|
+
{"type": "perl", "subs": [...]} s/// substitutions of perl -pe/-i
|
|
21
|
+
{"type": "py", "replaces": [...], "regex": [...], "line_ops": bool, "reads_self": bool}
|
|
22
|
+
{"type": "patch", "hunks": [...]} unified or apply_patch hunks
|
|
23
|
+
{"type": "ed", "script": str}
|
|
24
|
+
{"type": "awk", "program": str}
|
|
25
|
+
|
|
26
|
+
A read whose stdout goes to a file or into a counting/digest sink is not a
|
|
27
|
+
view. cwd changes (`cd`) are tracked through the command and returned.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import posixpath
|
|
31
|
+
import re
|
|
32
|
+
import shlex
|
|
33
|
+
from typing import Dict, List, Optional, Tuple
|
|
34
|
+
|
|
35
|
+
HEREDOC_RE = re.compile(r"""(?<!<)<<(-?)(?!<)\s*(\\?)(['"]?)([A-Za-z_][A-Za-z0-9_.-]*)\3""")
|
|
36
|
+
SEPS = {";", ";;", "&&", "||", "&", "\n"}
|
|
37
|
+
REDIR_TOKENS = {">", ">>", "<", "&>", "&>>", ">|", "2>", "2>>", "1>", "1>>", ">&", "2>&1", "<>"}
|
|
38
|
+
VIEW_VERBS = {"cat", "nl", "head", "tail", "less", "more", "bat", "batcat", "tac"}
|
|
39
|
+
GREP_VERBS = {"grep", "egrep", "fgrep", "rg", "ag", "ack", "zgrep"}
|
|
40
|
+
SINKS = {"wc", "md5sum", "sha1sum", "sha256sum", "cksum", "xxd", "od", "hexdump", "file", "stat", "true"}
|
|
41
|
+
LITERAL_SOURCES = {"echo", "printf"}
|
|
42
|
+
INTERP_PREFIX = ("python", "pypy")
|
|
43
|
+
WRAPPERS = {"sudo", "command", "exec", "nohup", "time", "env", "xargs", "nice", "stdbuf"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def split_heredocs(cmd: str) -> Tuple[str, List[str]]:
|
|
47
|
+
"""Remove heredoc bodies; replace each operator by a placeholder word."""
|
|
48
|
+
lines = (cmd or "").split("\n")
|
|
49
|
+
out, bodies, i = [], [], 0
|
|
50
|
+
while i < len(lines):
|
|
51
|
+
line = lines[i]
|
|
52
|
+
i += 1
|
|
53
|
+
ops = list(HEREDOC_RE.finditer(line))
|
|
54
|
+
if not ops:
|
|
55
|
+
out.append(line)
|
|
56
|
+
continue
|
|
57
|
+
new_line = line
|
|
58
|
+
for m in reversed(ops):
|
|
59
|
+
k = len(bodies) + (len(ops) - 1 - ops.index(m))
|
|
60
|
+
new_line = new_line[:m.start()] + f" __HEREDOC_{k}__ " + new_line[m.end():]
|
|
61
|
+
out.append(new_line)
|
|
62
|
+
for m in ops:
|
|
63
|
+
strip_tabs = m.group(1) == "-"
|
|
64
|
+
delim = m.group(4)
|
|
65
|
+
body = []
|
|
66
|
+
while i < len(lines):
|
|
67
|
+
ln = lines[i]
|
|
68
|
+
cmp = ln.lstrip("\t") if strip_tabs else ln
|
|
69
|
+
if cmp.strip() == delim:
|
|
70
|
+
i += 1
|
|
71
|
+
break
|
|
72
|
+
body.append(ln)
|
|
73
|
+
i += 1
|
|
74
|
+
bodies.append("\n".join(body))
|
|
75
|
+
return "\n".join(out), bodies
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mask_heredocs(cmd: str) -> str:
|
|
79
|
+
"""The command text with every heredoc body blanked (line count kept),
|
|
80
|
+
so positions of shell words can be searched without hitting payloads."""
|
|
81
|
+
lines = (cmd or "").split("\n")
|
|
82
|
+
out, i = [], 0
|
|
83
|
+
while i < len(lines):
|
|
84
|
+
line = lines[i]
|
|
85
|
+
out.append(line)
|
|
86
|
+
i += 1
|
|
87
|
+
for m in HEREDOC_RE.finditer(line):
|
|
88
|
+
strip_tabs = m.group(1) == "-"
|
|
89
|
+
delim = m.group(4)
|
|
90
|
+
while i < len(lines):
|
|
91
|
+
ln = lines[i]
|
|
92
|
+
cmp = ln.lstrip("\t") if strip_tabs else ln
|
|
93
|
+
out.append(" " * len(ln)) # same length: offsets stay valid
|
|
94
|
+
i += 1
|
|
95
|
+
if cmp.strip() == delim:
|
|
96
|
+
break
|
|
97
|
+
return "\n".join(out)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_OPS_RE = re.compile(r"\|\||&&|;;|&>>|>>|<<<|<<|&>|>&|<&|>\||[();<>|&\n]")
|
|
101
|
+
_PUNCT = set("();<>|&\n")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def tokenize(shell: str) -> List[str]:
|
|
105
|
+
"""shlex words; runs of punctuation (which shlex groups, e.g. ')\n')
|
|
106
|
+
are split back into single shell operators."""
|
|
107
|
+
shell = shell.replace("\\\n", " ")
|
|
108
|
+
try:
|
|
109
|
+
lx = shlex.shlex(shell, posix=True, punctuation_chars="();<>|&\n")
|
|
110
|
+
lx.whitespace = " \t\r"
|
|
111
|
+
lx.whitespace_split = True
|
|
112
|
+
lx.commenters = "#"
|
|
113
|
+
raw = list(lx)
|
|
114
|
+
except ValueError:
|
|
115
|
+
return []
|
|
116
|
+
out = []
|
|
117
|
+
for t in raw:
|
|
118
|
+
if t and set(t) <= _PUNCT and len(t) > 1 and t not in ("&&", "||", ";;", ">>", "<<", "&>", ">&", "<&", ">|", "&>>", "<<<"):
|
|
119
|
+
out.extend(_OPS_RE.findall(t))
|
|
120
|
+
else:
|
|
121
|
+
out.append(t)
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def simple_commands(cmd: str) -> List[Dict]:
|
|
126
|
+
"""Simple commands with argv, redirections, stdin heredoc body, pipeline
|
|
127
|
+
membership and the connector that precedes each one."""
|
|
128
|
+
shell, bodies = split_heredocs(cmd)
|
|
129
|
+
toks = tokenize(shell)
|
|
130
|
+
if not toks and shell.strip():
|
|
131
|
+
# Unbalanced quoting somewhere: tokenise line by line and keep the
|
|
132
|
+
# lines that parse (a stray apostrophe in one line must not hide the
|
|
133
|
+
# rest of the command).
|
|
134
|
+
toks = []
|
|
135
|
+
for line in shell.split("\n"):
|
|
136
|
+
lt = tokenize(line)
|
|
137
|
+
if lt:
|
|
138
|
+
toks.extend(lt + ["\n"])
|
|
139
|
+
if not toks:
|
|
140
|
+
return [{"argv": [], "redirs": [], "heredoc": None, "pipe": 0, "guard": ";", "depth": 0,
|
|
141
|
+
"unparsed": True}]
|
|
142
|
+
units, pipe, depth = [], 0, 0
|
|
143
|
+
guard = ";"
|
|
144
|
+
|
|
145
|
+
def new():
|
|
146
|
+
return {"argv": [], "redirs": [], "heredoc": None, "pipe": pipe, "guard": guard, "depth": depth}
|
|
147
|
+
|
|
148
|
+
cur = new()
|
|
149
|
+
pending = None
|
|
150
|
+
for t in toks:
|
|
151
|
+
m = re.fullmatch(r"__HEREDOC_(\d+)__", t)
|
|
152
|
+
if m:
|
|
153
|
+
k = int(m.group(1))
|
|
154
|
+
cur["heredoc"] = bodies[k] if k < len(bodies) else ""
|
|
155
|
+
continue
|
|
156
|
+
if pending is not None:
|
|
157
|
+
cur["redirs"].append((pending, t))
|
|
158
|
+
pending = None
|
|
159
|
+
continue
|
|
160
|
+
if t == "|":
|
|
161
|
+
if cur["argv"] or cur["redirs"]:
|
|
162
|
+
units.append(cur)
|
|
163
|
+
guard = "|"
|
|
164
|
+
cur = new()
|
|
165
|
+
continue
|
|
166
|
+
if t in ("(", ")"):
|
|
167
|
+
if cur["argv"] or cur["redirs"]:
|
|
168
|
+
units.append(cur)
|
|
169
|
+
depth += 1 if t == "(" else -1
|
|
170
|
+
units.append({"argv": [t], "redirs": [], "heredoc": None, "pipe": pipe, "guard": guard,
|
|
171
|
+
"depth": depth, "paren": True})
|
|
172
|
+
pipe += 1
|
|
173
|
+
guard = ";"
|
|
174
|
+
cur = new()
|
|
175
|
+
continue
|
|
176
|
+
if t in SEPS or (set(t) <= set(";&|\n") and "\n" in t):
|
|
177
|
+
if cur["argv"] or cur["redirs"]:
|
|
178
|
+
units.append(cur)
|
|
179
|
+
pipe += 1
|
|
180
|
+
tt = t.replace("\n", "")
|
|
181
|
+
guard = tt if tt in ("&&", "||") else ";"
|
|
182
|
+
cur = new()
|
|
183
|
+
continue
|
|
184
|
+
if set(t) <= set("<>&") and ("<" in t or ">" in t):
|
|
185
|
+
pending = t
|
|
186
|
+
continue
|
|
187
|
+
if re.fullmatch(r"\d?>&\d", t) or t in ("2>&1", ">&2", "1>&2"):
|
|
188
|
+
continue
|
|
189
|
+
cur["argv"].append(t)
|
|
190
|
+
if cur["argv"] or cur["redirs"]:
|
|
191
|
+
units.append(cur)
|
|
192
|
+
# Fold "2" fd prefixes: tokenizer yields "2" then ">" for "2>"; drop a bare
|
|
193
|
+
# digit argv word that is followed by a redirection of the same unit.
|
|
194
|
+
for u in units:
|
|
195
|
+
if u["argv"] and u["redirs"] and re.fullmatch(r"[0-9]", u["argv"][-1]):
|
|
196
|
+
u["argv"] = u["argv"][:-1]
|
|
197
|
+
return units
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def strip_prefixes(argv: List[str]) -> List[str]:
|
|
201
|
+
argv = [a for a in argv if a != ""]
|
|
202
|
+
while argv and (re.match(r"^[A-Za-z_]\w*=", argv[0]) or argv[0] in WRAPPERS):
|
|
203
|
+
if argv[0] == "env":
|
|
204
|
+
argv = argv[1:]
|
|
205
|
+
while argv and (argv[0].startswith("-") or re.match(r"^[A-Za-z_]\w*=", argv[0])):
|
|
206
|
+
argv = argv[1:]
|
|
207
|
+
continue
|
|
208
|
+
argv = argv[1:]
|
|
209
|
+
if argv and argv[0] == "timeout":
|
|
210
|
+
rest = argv[1:]
|
|
211
|
+
while rest and rest[0].startswith("-"):
|
|
212
|
+
rest = rest[1:]
|
|
213
|
+
argv = rest[1:] if rest else []
|
|
214
|
+
return argv
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def path_like(tok: str) -> bool:
|
|
218
|
+
if not tok or tok.startswith("-") or tok in (".", "..", "/", "~"):
|
|
219
|
+
return False
|
|
220
|
+
if tok.startswith(("/dev/", "http://", "https://")) or tok == "/dev/null":
|
|
221
|
+
return False
|
|
222
|
+
if tok.startswith("/proc/") and not tok.startswith("/proc/self/cwd/"):
|
|
223
|
+
return False
|
|
224
|
+
if any(c in tok for c in "{}$*?!()<>\"'`=\n|;"):
|
|
225
|
+
return False
|
|
226
|
+
if tok.isdigit():
|
|
227
|
+
return False
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def resolve(path: str, cwd: str) -> str:
|
|
232
|
+
if path.startswith("~"):
|
|
233
|
+
path = "/root" + path[1:]
|
|
234
|
+
if path.startswith("/proc/self/cwd/"):
|
|
235
|
+
# the shell's working directory, spelled through procfs
|
|
236
|
+
path = posixpath.join(cwd or "/", path[len("/proc/self/cwd/"):])
|
|
237
|
+
p = path if path.startswith("/") else posixpath.join(cwd or "/", path)
|
|
238
|
+
return posixpath.normpath(p)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- sed ---------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _read_delimited(s: str, i: int, d: str) -> Tuple[str, int]:
|
|
244
|
+
"""Read text up to the next unescaped delimiter d starting at s[i]."""
|
|
245
|
+
out = []
|
|
246
|
+
while i < len(s):
|
|
247
|
+
c = s[i]
|
|
248
|
+
if c == "\\" and i + 1 < len(s):
|
|
249
|
+
if s[i + 1] == d:
|
|
250
|
+
out.append(d)
|
|
251
|
+
else:
|
|
252
|
+
out.append(c + s[i + 1])
|
|
253
|
+
i += 2
|
|
254
|
+
continue
|
|
255
|
+
if c == d:
|
|
256
|
+
return "".join(out), i + 1
|
|
257
|
+
out.append(c)
|
|
258
|
+
i += 1
|
|
259
|
+
return "".join(out), -1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def parse_sed_script(script: str) -> List[Dict]:
|
|
263
|
+
"""Parse a sed script into commands with addresses. Unknown syntax gives
|
|
264
|
+
an entry with cmd '?'."""
|
|
265
|
+
cmds = []
|
|
266
|
+
i, n = 0, len(script)
|
|
267
|
+
while i < n:
|
|
268
|
+
while i < n and script[i] in " \t\n;":
|
|
269
|
+
i += 1
|
|
270
|
+
if i >= n:
|
|
271
|
+
break
|
|
272
|
+
addr = []
|
|
273
|
+
for k in range(2):
|
|
274
|
+
if k == 1 and i + 1 < n and script[i] in "+~" and script[i + 1].isdigit():
|
|
275
|
+
# GNU extension: addr,+N (N lines after) or addr,~N (to a multiple of N)
|
|
276
|
+
j = i + 1
|
|
277
|
+
while j < n and script[j].isdigit():
|
|
278
|
+
j += 1
|
|
279
|
+
addr.append({"rel" if script[i] == "+" else "mult": int(script[i + 1:j])})
|
|
280
|
+
i = j
|
|
281
|
+
break
|
|
282
|
+
if i < n and script[i].isdigit():
|
|
283
|
+
j = i
|
|
284
|
+
while j < n and (script[j].isdigit()):
|
|
285
|
+
j += 1
|
|
286
|
+
addr.append({"line": int(script[i:j])})
|
|
287
|
+
i = j
|
|
288
|
+
elif i < n and script[i] == "$":
|
|
289
|
+
addr.append({"line": "$"})
|
|
290
|
+
i += 1
|
|
291
|
+
elif i < n and script[i] in "/\\":
|
|
292
|
+
d = "/"
|
|
293
|
+
if script[i] == "\\" and i + 1 < n:
|
|
294
|
+
d = script[i + 1]
|
|
295
|
+
i += 1
|
|
296
|
+
pat, j = _read_delimited(script, i + 1, d)
|
|
297
|
+
if j < 0:
|
|
298
|
+
return cmds + [{"cmd": "?", "raw": script[i:]}]
|
|
299
|
+
addr.append({"re": pat})
|
|
300
|
+
i = j
|
|
301
|
+
while i < n and script[i] in "I":
|
|
302
|
+
i += 1
|
|
303
|
+
else:
|
|
304
|
+
break
|
|
305
|
+
if i < n and script[i] == ",":
|
|
306
|
+
i += 1
|
|
307
|
+
continue
|
|
308
|
+
break
|
|
309
|
+
while i < n and script[i] in " \t":
|
|
310
|
+
i += 1
|
|
311
|
+
if i < n and script[i] == "!":
|
|
312
|
+
i += 1
|
|
313
|
+
if i >= n:
|
|
314
|
+
cmds.append({"addr": addr, "cmd": "p"})
|
|
315
|
+
break
|
|
316
|
+
c = script[i]
|
|
317
|
+
if c == "s" and i + 1 < n:
|
|
318
|
+
d = script[i + 1]
|
|
319
|
+
pat, j = _read_delimited(script, i + 2, d)
|
|
320
|
+
if j < 0:
|
|
321
|
+
return cmds + [{"cmd": "?", "raw": script[i:]}]
|
|
322
|
+
rep, k = _read_delimited(script, j, d)
|
|
323
|
+
if k < 0:
|
|
324
|
+
return cmds + [{"cmd": "?", "raw": script[i:]}]
|
|
325
|
+
m = re.match(r"[gpiIeEmM0-9w]*", script[k:])
|
|
326
|
+
flags = m.group(0) if m else ""
|
|
327
|
+
cmds.append({"addr": addr, "cmd": "s", "pat": pat, "rep": rep, "flags": flags})
|
|
328
|
+
i = k + len(flags)
|
|
329
|
+
elif c in "aic":
|
|
330
|
+
j = i + 1
|
|
331
|
+
backslash = False
|
|
332
|
+
if j < n and script[j] == "\\":
|
|
333
|
+
j += 1
|
|
334
|
+
backslash = True
|
|
335
|
+
if j < n and script[j] == "\n":
|
|
336
|
+
j += 1
|
|
337
|
+
while j < n and script[j] in " \t" and not backslash:
|
|
338
|
+
j += 1
|
|
339
|
+
# GNU one-liner: text runs to end of line (or end of script)
|
|
340
|
+
end = script.find("\n", j)
|
|
341
|
+
text_lines = []
|
|
342
|
+
if end < 0:
|
|
343
|
+
text_lines.append(script[j:])
|
|
344
|
+
i = n
|
|
345
|
+
else:
|
|
346
|
+
text_lines.append(script[j:end])
|
|
347
|
+
i = end + 1
|
|
348
|
+
# continuation lines ending with backslash
|
|
349
|
+
while text_lines[-1].endswith("\\") and i < n:
|
|
350
|
+
text_lines[-1] = text_lines[-1][:-1]
|
|
351
|
+
end = script.find("\n", i)
|
|
352
|
+
if end < 0:
|
|
353
|
+
text_lines.append(script[i:])
|
|
354
|
+
i = n
|
|
355
|
+
else:
|
|
356
|
+
text_lines.append(script[i:end])
|
|
357
|
+
i = end + 1
|
|
358
|
+
text = "\n".join(text_lines).replace("\\n", "\n")
|
|
359
|
+
cmds.append({"addr": addr, "cmd": c, "text": text})
|
|
360
|
+
elif c in "dpPDNnqQ=hHgGxlz":
|
|
361
|
+
cmds.append({"addr": addr, "cmd": c})
|
|
362
|
+
i += 1
|
|
363
|
+
elif c == "y" and i + 1 < n:
|
|
364
|
+
d = script[i + 1]
|
|
365
|
+
a, j = _read_delimited(script, i + 2, d)
|
|
366
|
+
b, k = _read_delimited(script, j, d) if j > 0 else ("", -1)
|
|
367
|
+
cmds.append({"addr": addr, "cmd": "y", "a": a, "b": b})
|
|
368
|
+
i = k if k > 0 else n
|
|
369
|
+
elif c in "{}":
|
|
370
|
+
cmds.append({"addr": addr, "cmd": c})
|
|
371
|
+
i += 1
|
|
372
|
+
elif c in "rwRW":
|
|
373
|
+
end = script.find("\n", i)
|
|
374
|
+
cmds.append({"addr": addr, "cmd": c, "file": script[i + 1:end if end > 0 else n].strip()})
|
|
375
|
+
i = end + 1 if end > 0 else n
|
|
376
|
+
else:
|
|
377
|
+
cmds.append({"cmd": "?", "raw": script[i:]})
|
|
378
|
+
break
|
|
379
|
+
return cmds
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _sed_parts(argv: List[str]):
|
|
383
|
+
"""(script_texts, files, inplace, quiet, has_z, has_E)."""
|
|
384
|
+
scripts, files = [], []
|
|
385
|
+
inplace = quiet = has_z = has_E = False
|
|
386
|
+
i = 1
|
|
387
|
+
had_e = False
|
|
388
|
+
while i < len(argv):
|
|
389
|
+
a = argv[i]
|
|
390
|
+
if a in ("-e", "--expression") and i + 1 < len(argv):
|
|
391
|
+
scripts.append(argv[i + 1])
|
|
392
|
+
had_e = True
|
|
393
|
+
i += 2
|
|
394
|
+
continue
|
|
395
|
+
if a.startswith("--expression="):
|
|
396
|
+
scripts.append(a.split("=", 1)[1])
|
|
397
|
+
had_e = True
|
|
398
|
+
elif a == "-f" and i + 1 < len(argv):
|
|
399
|
+
scripts.append("__FILE__:" + argv[i + 1])
|
|
400
|
+
had_e = True
|
|
401
|
+
i += 2
|
|
402
|
+
continue
|
|
403
|
+
elif a.startswith("--in-place") or (a.startswith("-") and not a.startswith("--") and "i" in a[1:].split("=")[0][:1] + ("i" if re.match(r"^-[a-zA-Z]*i", a) else "")):
|
|
404
|
+
inplace = True
|
|
405
|
+
if re.match(r"^-[a-zA-Z]*n", a):
|
|
406
|
+
quiet = True
|
|
407
|
+
if "E" in a or "r" in a[1:2]:
|
|
408
|
+
has_E = True
|
|
409
|
+
elif a in ("-n", "--quiet", "--silent") or re.match(r"^-[a-zA-Z]*n[a-zA-Z]*$", a):
|
|
410
|
+
quiet = True
|
|
411
|
+
if "i" in a:
|
|
412
|
+
inplace = True
|
|
413
|
+
if "E" in a or "r" in a:
|
|
414
|
+
has_E = True
|
|
415
|
+
elif a in ("-z", "--null-data") or re.match(r"^-[a-zA-Z]*z", a):
|
|
416
|
+
has_z = True
|
|
417
|
+
elif a in ("-E", "-r", "--regexp-extended"):
|
|
418
|
+
has_E = True
|
|
419
|
+
elif a.startswith("-") and len(a) > 1:
|
|
420
|
+
if re.match(r"^-[a-zA-Z]+$", a):
|
|
421
|
+
if "i" in a:
|
|
422
|
+
inplace = True
|
|
423
|
+
if "n" in a:
|
|
424
|
+
quiet = True
|
|
425
|
+
if "E" in a or "r" in a:
|
|
426
|
+
has_E = True
|
|
427
|
+
if "z" in a:
|
|
428
|
+
has_z = True
|
|
429
|
+
elif a == "" and inplace:
|
|
430
|
+
pass # BSD backup suffix
|
|
431
|
+
else:
|
|
432
|
+
if not had_e and not scripts:
|
|
433
|
+
scripts.append(a)
|
|
434
|
+
else:
|
|
435
|
+
files.append(a)
|
|
436
|
+
i += 1
|
|
437
|
+
return scripts, files, inplace, quiet, has_z, has_E
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def sed_view_range(cmds: List[Dict]) -> Optional[Tuple[int, int]]:
|
|
441
|
+
"""For `sed -n 'a,bp'` (or 'Np'), the printed line range."""
|
|
442
|
+
ps = [c for c in cmds if c.get("cmd") == "p"]
|
|
443
|
+
if len(ps) != 1 or len(cmds) > 2:
|
|
444
|
+
return None
|
|
445
|
+
addr = ps[0].get("addr") or []
|
|
446
|
+
if len(addr) == 1 and isinstance(addr[0].get("line"), int):
|
|
447
|
+
return addr[0]["line"], addr[0]["line"]
|
|
448
|
+
if len(addr) == 2 and isinstance(addr[0].get("line"), int):
|
|
449
|
+
if isinstance(addr[1].get("rel"), int):
|
|
450
|
+
return addr[0]["line"], addr[0]["line"] + addr[1]["rel"]
|
|
451
|
+
b = addr[1].get("line")
|
|
452
|
+
return addr[0]["line"], (b if isinstance(b, int) else 10 ** 9)
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# --- python scripts ------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
_PY_STR = r"""(?:[rbfuRBFU]{0,2})(?:'''(?:.|\n)*?'''|\"\"\"(?:.|\n)*?\"\"\"|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*")"""
|
|
459
|
+
_PY_REPLACE = re.compile(r"\.replace\(\s*(" + _PY_STR + r")\s*,\s*(" + _PY_STR + r")")
|
|
460
|
+
_PY_RESUB = re.compile(r"re\.sub[n]?\(\s*(" + _PY_STR + r")\s*,\s*(" + _PY_STR + r")?")
|
|
461
|
+
_PY_OPEN = re.compile(r"""open\(\s*(?P<arg>[^,)]+?)\s*(?:,\s*(?:mode\s*=\s*)?(?P<mode>[rbfu]?['"][^'"]*['"]))?\s*[,)]""")
|
|
462
|
+
_PY_ASSIGN = re.compile(r"""(?P<v>\b[A-Za-z_]\w*)\s*=\s*(?:(?:pathlib\.)?Path\(\s*)?(?P<s>[rbfu]?(['"])(?:[^'"\n]+)\3)""")
|
|
463
|
+
_PY_PATHWRITE = re.compile(r"""(?:(?:pathlib\.)?Path\(\s*(?P<lit>[rbfu]?(['"])[^'"\n]+\2)\s*\)|(?P<v>\b[A-Za-z_]\w*))\s*\.write_(?:text|bytes)\(""")
|
|
464
|
+
_PY_PATHREAD = re.compile(r"""(?:(?:pathlib\.)?Path\(\s*(?P<lit>[rbfu]?(['"])[^'"\n]+\2)\s*\)|(?P<v>\b[A-Za-z_]\w*))\s*\.read_(?:text|bytes)\(""")
|
|
465
|
+
_PY_LINEOPS = re.compile(r"\w*lines\[\s*[\w+\- ]+\]\s*=|\.insert\(\s*[\w+\- ]+,|del\s+\w+\[|\w+\[\s*\d+\s*:\s*\d+\s*\]|splitlines\(\)\[")
|
|
466
|
+
_PY_FIND = re.compile(r"\.(?:find|index|rfind|rindex|startswith|count)\(\s*(" + _PY_STR + r")|(" + _PY_STR + r")\s+(?:not\s+)?in\s+\w+")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def py_literal(tok: str) -> Optional[str]:
|
|
470
|
+
try:
|
|
471
|
+
import ast
|
|
472
|
+
import warnings
|
|
473
|
+
with warnings.catch_warnings():
|
|
474
|
+
warnings.simplefilter("ignore")
|
|
475
|
+
v = ast.literal_eval(tok)
|
|
476
|
+
if isinstance(v, bytes):
|
|
477
|
+
v = v.decode("utf-8", "replace")
|
|
478
|
+
return v if isinstance(v, str) else None
|
|
479
|
+
except Exception:
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def python_script_ops(script: str, cwd: str, strict: bool = False) -> Tuple[List[Dict], List[str]]:
|
|
484
|
+
"""(write ops, read paths) of a Python script, by literal or variable."""
|
|
485
|
+
if not script:
|
|
486
|
+
return [], []
|
|
487
|
+
names: Dict[str, str] = {}
|
|
488
|
+
for m in _PY_ASSIGN.finditer(script):
|
|
489
|
+
lit = py_literal(m.group("s"))
|
|
490
|
+
if lit:
|
|
491
|
+
names[m.group("v")] = lit
|
|
492
|
+
|
|
493
|
+
def arg_path(arg: str) -> Optional[str]:
|
|
494
|
+
arg = arg.strip()
|
|
495
|
+
lit = py_literal(arg)
|
|
496
|
+
if lit is not None:
|
|
497
|
+
return lit
|
|
498
|
+
m = re.fullmatch(r"(?:pathlib\.)?Path\(\s*(" + _PY_STR + r")\s*\)", arg)
|
|
499
|
+
if m:
|
|
500
|
+
return py_literal(m.group(1))
|
|
501
|
+
if re.fullmatch(r"[A-Za-z_]\w*", arg):
|
|
502
|
+
return names.get(arg)
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
writes, reads = [], []
|
|
506
|
+
wline: Dict[str, int] = {}
|
|
507
|
+
|
|
508
|
+
def line_of(pos: int) -> int:
|
|
509
|
+
return script.count("\n", 0, pos) + 1
|
|
510
|
+
|
|
511
|
+
for m in _PY_OPEN.finditer(script):
|
|
512
|
+
p = arg_path(m.group("arg"))
|
|
513
|
+
if not p:
|
|
514
|
+
continue
|
|
515
|
+
mode = py_literal(m.group("mode")) if m.group("mode") else "r"
|
|
516
|
+
mode = mode or "r"
|
|
517
|
+
if any(c in mode for c in "wa+x"):
|
|
518
|
+
writes.append((p, "a" in mode))
|
|
519
|
+
wline[p] = min(wline.get(p, 10 ** 9), line_of(m.start()))
|
|
520
|
+
else:
|
|
521
|
+
reads.append(p)
|
|
522
|
+
for m in _PY_PATHWRITE.finditer(script):
|
|
523
|
+
p = py_literal(m.group("lit")) if m.group("lit") else names.get(m.group("v") or "")
|
|
524
|
+
if p:
|
|
525
|
+
writes.append((p, False))
|
|
526
|
+
wline[p] = min(wline.get(p, 10 ** 9), line_of(m.start()))
|
|
527
|
+
for m in _PY_PATHREAD.finditer(script):
|
|
528
|
+
p = py_literal(m.group("lit")) if m.group("lit") else names.get(m.group("v") or "")
|
|
529
|
+
if p:
|
|
530
|
+
reads.append(p)
|
|
531
|
+
replaces = []
|
|
532
|
+
for m in _PY_REPLACE.finditer(script):
|
|
533
|
+
a, b = py_literal(m.group(1)), py_literal(m.group(2))
|
|
534
|
+
if a is not None and b is not None:
|
|
535
|
+
replaces.append({"old": a, "new": b})
|
|
536
|
+
regex = []
|
|
537
|
+
for m in _PY_RESUB.finditer(script):
|
|
538
|
+
a = py_literal(m.group(1))
|
|
539
|
+
b = py_literal(m.group(2)) if m.group(2) else None
|
|
540
|
+
if a is not None:
|
|
541
|
+
regex.append({"pat": a, "rep": b})
|
|
542
|
+
# compiled patterns: pat = re.compile(LIT) ... pat.sub(REP, text)
|
|
543
|
+
compiled = {}
|
|
544
|
+
for m in re.finditer(r"(\b[A-Za-z_]\w*)\s*=\s*re\.compile\(\s*(" + _PY_STR + r")", script):
|
|
545
|
+
lit = py_literal(m.group(2))
|
|
546
|
+
if lit is not None:
|
|
547
|
+
compiled[m.group(1)] = lit
|
|
548
|
+
for m in re.finditer(r"(\b[A-Za-z_]\w*)\.subn?\(\s*(" + _PY_STR + r")?", script):
|
|
549
|
+
if m.group(1) in compiled:
|
|
550
|
+
regex.append({"pat": compiled[m.group(1)], "rep": py_literal(m.group(2)) if m.group(2) else None})
|
|
551
|
+
line_ops = bool(_PY_LINEOPS.search(script))
|
|
552
|
+
finds = []
|
|
553
|
+
for m in _PY_FIND.finditer(script):
|
|
554
|
+
lit = py_literal(m.group(1) or m.group(2) or "")
|
|
555
|
+
if lit and len(lit) >= 3:
|
|
556
|
+
finds.append(lit)
|
|
557
|
+
ops = []
|
|
558
|
+
read_set = {resolve(r, cwd) for r in reads if path_like(r)}
|
|
559
|
+
for p, append in dict.fromkeys(writes):
|
|
560
|
+
if not path_like(p):
|
|
561
|
+
continue
|
|
562
|
+
ap = resolve(p, cwd)
|
|
563
|
+
first_write_line = wline.get(p)
|
|
564
|
+
self_read = ap in read_set
|
|
565
|
+
if append:
|
|
566
|
+
ops.append({"op": "W", "path": ap, "kind": "append", "mech": "python",
|
|
567
|
+
"anchor": None, "write_line": first_write_line})
|
|
568
|
+
elif self_read and (replaces or regex or (not strict and (line_ops or finds))):
|
|
569
|
+
ops.append({"op": "W", "path": ap, "kind": "anchored", "mech": "python",
|
|
570
|
+
"anchor": {"type": "py", "replaces": replaces, "regex": regex, "finds": finds[:20],
|
|
571
|
+
"line_ops": line_ops, "reads_self": True},
|
|
572
|
+
"write_line": first_write_line})
|
|
573
|
+
elif self_read:
|
|
574
|
+
# read-transform-write with no textual anchor (json.load/dump, a
|
|
575
|
+
# program updating its data file): a whole-file rewrite
|
|
576
|
+
ops.append({"op": "W", "path": ap, "kind": "rewrite", "mech": "python",
|
|
577
|
+
"anchor": None, "write_line": first_write_line})
|
|
578
|
+
else:
|
|
579
|
+
ops.append({"op": "W", "path": ap, "kind": "overwrite_unseen", "mech": "python",
|
|
580
|
+
"anchor": None, "write_line": first_write_line})
|
|
581
|
+
return ops, sorted(read_set)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# --- patches ---------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
_HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def parse_unified(body: str, cwd: str, strip: int = 1) -> List[Dict]:
|
|
590
|
+
"""Per-file hunks of a unified diff (or OpenAI apply_patch envelope)."""
|
|
591
|
+
out: Dict[str, Dict] = {}
|
|
592
|
+
cur = None
|
|
593
|
+
hunk = None
|
|
594
|
+
lines = (body or "").split("\n")
|
|
595
|
+
for ln in lines:
|
|
596
|
+
if ln.startswith("+++ "):
|
|
597
|
+
p = ln[4:].split("\t")[0].strip()
|
|
598
|
+
if p == "/dev/null":
|
|
599
|
+
cur = None
|
|
600
|
+
continue
|
|
601
|
+
parts = p.split("/")
|
|
602
|
+
p = "/".join(parts[strip:]) if strip and len(parts) > strip and p.startswith(("a/", "b/")) else p
|
|
603
|
+
cur = out.setdefault(resolve(p, cwd), {"hunks": []})
|
|
604
|
+
hunk = None
|
|
605
|
+
elif ln.startswith("*** Update File:") or ln.startswith("*** Add File:"):
|
|
606
|
+
p = ln.split(":", 1)[1].strip()
|
|
607
|
+
cur = out.setdefault(resolve(p, cwd), {"hunks": [], "envelope": ln.split(":")[0][4:]})
|
|
608
|
+
hunk = None
|
|
609
|
+
elif ln.startswith("--- ") or ln.startswith("diff ") or ln.startswith("index ") or ln.startswith("*** "):
|
|
610
|
+
continue
|
|
611
|
+
elif cur is not None and (ln.startswith("@@")):
|
|
612
|
+
m = _HUNK_RE.match(ln)
|
|
613
|
+
hunk = {"old_start": int(m.group(1)) if m else None, "new_start": int(m.group(3)) if m else None,
|
|
614
|
+
"lines": []}
|
|
615
|
+
cur["hunks"].append(hunk)
|
|
616
|
+
elif cur is not None and ln[:1] in (" ", "+", "-"):
|
|
617
|
+
if hunk is None:
|
|
618
|
+
hunk = {"old_start": None, "new_start": None, "lines": []}
|
|
619
|
+
cur["hunks"].append(hunk)
|
|
620
|
+
hunk["lines"].append(ln)
|
|
621
|
+
return [{"path": p, **v} for p, v in out.items()]
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def hunk_texts(h: Dict) -> Tuple[str, str]:
|
|
625
|
+
old = [ln[1:] for ln in h["lines"] if ln[:1] in (" ", "-")]
|
|
626
|
+
new = [ln[1:] for ln in h["lines"] if ln[:1] in (" ", "+")]
|
|
627
|
+
return "\n".join(old), "\n".join(new)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# --- main extractor ---------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
def _grep_parts(argv):
|
|
633
|
+
flags, operands = [], []
|
|
634
|
+
i = 1
|
|
635
|
+
arg_flags = {"-e", "-f", "--regexp", "-A", "-B", "-C", "-m", "--max-count", "--include", "--exclude",
|
|
636
|
+
"--exclude-dir", "-g", "--glob", "-t", "--type", "-d", "-D"}
|
|
637
|
+
pats = []
|
|
638
|
+
while i < len(argv):
|
|
639
|
+
a = argv[i]
|
|
640
|
+
if a in arg_flags and i + 1 < len(argv):
|
|
641
|
+
if a in ("-e", "--regexp"):
|
|
642
|
+
pats.append(argv[i + 1])
|
|
643
|
+
flags.append(a)
|
|
644
|
+
i += 2
|
|
645
|
+
continue
|
|
646
|
+
if a.startswith("-") and len(a) > 1:
|
|
647
|
+
flags.append(a)
|
|
648
|
+
else:
|
|
649
|
+
operands.append(a)
|
|
650
|
+
i += 1
|
|
651
|
+
if not pats and operands:
|
|
652
|
+
pats.append(operands[0])
|
|
653
|
+
operands = operands[1:]
|
|
654
|
+
no_content = any(f in ("-l", "-L", "-c", "-q", "--files-with-matches", "--files-without-match", "--count",
|
|
655
|
+
"--quiet", "--silent") or (re.match(r"^-[a-zA-Z]+$", f) and set(f[1:]) & set("lLcq"))
|
|
656
|
+
for f in flags)
|
|
657
|
+
numbered = any(f in ("-n", "--line-number") or (re.match(r"^-[a-zA-Z]+$", f) and "n" in f) for f in flags)
|
|
658
|
+
return operands, no_content, numbered, pats
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def head_tail_n(argv) -> Optional[int]:
|
|
662
|
+
for i, a in enumerate(argv[1:], 1):
|
|
663
|
+
m = re.fullmatch(r"-n\s*(\d+)|-(\d+)|--lines=(\d+)", a)
|
|
664
|
+
if m:
|
|
665
|
+
return int(next(g for g in m.groups() if g))
|
|
666
|
+
if a == "-n" and i + 1 < len(argv) and re.fullmatch(r"\+?\d+", argv[i + 1]):
|
|
667
|
+
return int(argv[i + 1].lstrip("+"))
|
|
668
|
+
return None
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
_DEPTH = [0]
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def extract(cmd: str, cwd: str, known_files: Optional[Dict[str, str]] = None,
|
|
675
|
+
persist_cd: bool = True) -> Tuple[List[Dict], str, Dict]:
|
|
676
|
+
"""File operations of one shell command. Returns (ops, cwd_after, info).
|
|
677
|
+
|
|
678
|
+
known_files maps absolute path -> text for files whose content the agent
|
|
679
|
+
wrote itself (heredoc scripts), so `python3 fix.py` can be analysed."""
|
|
680
|
+
known_files = known_files if known_files is not None else {}
|
|
681
|
+
units = simple_commands(cmd)
|
|
682
|
+
info = {"unparsed": any(u.get("unparsed") for u in units), "n_units": len(units), "verbs": []}
|
|
683
|
+
ops: List[Dict] = []
|
|
684
|
+
cwd0 = cwd
|
|
685
|
+
stack = []
|
|
686
|
+
# which pipes end in a sink or a no-content grep
|
|
687
|
+
last_in_pipe: Dict[int, Dict] = {}
|
|
688
|
+
for u in units:
|
|
689
|
+
if not u.get("paren"):
|
|
690
|
+
last_in_pipe[u["pipe"]] = u
|
|
691
|
+
pipe_members: Dict[int, List[Dict]] = {}
|
|
692
|
+
for u in units:
|
|
693
|
+
pipe_members.setdefault(u["pipe"], []).append(u)
|
|
694
|
+
|
|
695
|
+
def pipe_swallows(u) -> bool:
|
|
696
|
+
"""Content produced by u does not reach the agent."""
|
|
697
|
+
members = pipe_members.get(u["pipe"], [])
|
|
698
|
+
idx = members.index(u)
|
|
699
|
+
downstream = members[idx + 1:]
|
|
700
|
+
# redirected stdout of u itself
|
|
701
|
+
for op, t in u["redirs"]:
|
|
702
|
+
if op in (">", ">>", ">|", "&>", "&>>", "1>", "1>>") and t not in ("/dev/stderr",):
|
|
703
|
+
return True
|
|
704
|
+
for v in downstream:
|
|
705
|
+
argv = strip_prefixes(v["argv"])
|
|
706
|
+
verb = argv[0].split("/")[-1] if argv else ""
|
|
707
|
+
if verb in SINKS:
|
|
708
|
+
return True
|
|
709
|
+
if verb in GREP_VERBS:
|
|
710
|
+
_, no_content, _, _ = _grep_parts(argv)
|
|
711
|
+
if no_content:
|
|
712
|
+
return True
|
|
713
|
+
if verb in ("tee",) and all(t.startswith("/dev/") for t in argv[1:] if not t.startswith("-")) is False:
|
|
714
|
+
# tee writes a copy but still prints: content reaches the agent
|
|
715
|
+
pass
|
|
716
|
+
if downstream:
|
|
717
|
+
last = downstream[-1]
|
|
718
|
+
for op, t in last["redirs"]:
|
|
719
|
+
if op in (">", ">>", ">|", "&>", "&>>", "1>", "1>>") and t != "/dev/stderr":
|
|
720
|
+
return True
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
def filtered(u) -> bool:
|
|
724
|
+
"""Content passes through a filter (grep/head/tail/sed/awk) downstream."""
|
|
725
|
+
members = pipe_members.get(u["pipe"], [])
|
|
726
|
+
idx = members.index(u)
|
|
727
|
+
for v in members[idx + 1:]:
|
|
728
|
+
argv = strip_prefixes(v["argv"])
|
|
729
|
+
verb = argv[0].split("/")[-1] if argv else ""
|
|
730
|
+
if verb in GREP_VERBS or verb in ("head", "tail", "sed", "awk", "cut", "sort", "uniq", "tr", "less", "more"):
|
|
731
|
+
return True
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
literal_pipes = set()
|
|
735
|
+
for u in units:
|
|
736
|
+
argv = strip_prefixes(u["argv"])
|
|
737
|
+
verb = argv[0].split("/")[-1] if argv else ""
|
|
738
|
+
if verb in LITERAL_SOURCES or (verb == "cat" and u["heredoc"] is not None and
|
|
739
|
+
not [a for a in argv[1:] if not a.startswith("-")]):
|
|
740
|
+
literal_pipes.add(u["pipe"])
|
|
741
|
+
|
|
742
|
+
for ui, u in enumerate(units):
|
|
743
|
+
if u.get("paren"):
|
|
744
|
+
if u["argv"][0] == "(":
|
|
745
|
+
stack.append(cwd)
|
|
746
|
+
elif stack:
|
|
747
|
+
cwd = stack.pop()
|
|
748
|
+
continue
|
|
749
|
+
argv = strip_prefixes(u["argv"])
|
|
750
|
+
verb = argv[0].split("/")[-1] if argv else ""
|
|
751
|
+
info["verbs"].append(verb)
|
|
752
|
+
guard = u["guard"]
|
|
753
|
+
literal = u["pipe"] in literal_pipes
|
|
754
|
+
new_ops: List[Dict] = []
|
|
755
|
+
operands = [a for a in argv[1:] if not a.startswith("-")]
|
|
756
|
+
|
|
757
|
+
if verb == "cd":
|
|
758
|
+
tgt = operands[0] if operands else "~"
|
|
759
|
+
if tgt == "-" or any(c in tgt for c in "$`*?{}"):
|
|
760
|
+
continue # previous or computed directory: unknown, keep cwd
|
|
761
|
+
cwd = resolve(tgt, cwd)
|
|
762
|
+
continue
|
|
763
|
+
if verb in ("pushd",) and operands:
|
|
764
|
+
stack.append(cwd)
|
|
765
|
+
cwd = resolve(operands[0], cwd)
|
|
766
|
+
continue
|
|
767
|
+
if verb == "popd" and stack:
|
|
768
|
+
cwd = stack.pop()
|
|
769
|
+
continue
|
|
770
|
+
|
|
771
|
+
swallowed = pipe_swallows(u)
|
|
772
|
+
if verb in VIEW_VERBS:
|
|
773
|
+
files = [a for a in operands if path_like(a)]
|
|
774
|
+
if verb in ("head", "tail"):
|
|
775
|
+
files = [a for a in argv[1:] if path_like(a) and not re.fullmatch(r"\+?\d+", a)]
|
|
776
|
+
# drop the numeric argument of -n
|
|
777
|
+
cleaned = []
|
|
778
|
+
skip = False
|
|
779
|
+
for a in argv[1:]:
|
|
780
|
+
if skip:
|
|
781
|
+
skip = False
|
|
782
|
+
continue
|
|
783
|
+
if a in ("-n", "-c", "--lines", "--bytes"):
|
|
784
|
+
skip = True
|
|
785
|
+
continue
|
|
786
|
+
if not a.startswith("-") and path_like(a):
|
|
787
|
+
cleaned.append(a)
|
|
788
|
+
files = cleaned
|
|
789
|
+
for op_, t in u["redirs"]:
|
|
790
|
+
if op_ == "<":
|
|
791
|
+
files.append(t)
|
|
792
|
+
for f in files:
|
|
793
|
+
if swallowed:
|
|
794
|
+
continue
|
|
795
|
+
view = {}
|
|
796
|
+
if verb == "head":
|
|
797
|
+
n = head_tail_n(argv) or 10
|
|
798
|
+
view = {"range": [1, n]}
|
|
799
|
+
elif verb == "tail":
|
|
800
|
+
view = {"tail": head_tail_n(argv) or 10}
|
|
801
|
+
elif verb in ("cat", "nl", "bat", "batcat", "less", "more", "tac"):
|
|
802
|
+
view = {"range": [1, 10 ** 9]}
|
|
803
|
+
if verb == "nl" or "-n" in argv:
|
|
804
|
+
view["numbered"] = True
|
|
805
|
+
kind = "full" if view.get("range") == [1, 10 ** 9] else "range"
|
|
806
|
+
if filtered(u):
|
|
807
|
+
kind, view = "lines", {"filtered": True}
|
|
808
|
+
new_ops.append({"op": "R", "path": resolve(f, cwd), "kind": kind, "mech": verb, "view": view})
|
|
809
|
+
elif verb in GREP_VERBS:
|
|
810
|
+
files, no_content, numbered, pats = _grep_parts(argv)
|
|
811
|
+
recursive = any(re.match(r"^-[a-zA-Z]*[rR]", f) or f in ("--recursive",) for f in argv[1:]
|
|
812
|
+
if f.startswith("-")) or verb in ("rg", "ag")
|
|
813
|
+
if not no_content and not swallowed:
|
|
814
|
+
for f in files:
|
|
815
|
+
if path_like(f):
|
|
816
|
+
new_ops.append({"op": "R", "path": resolve(f, cwd), "kind": "lines", "mech": verb,
|
|
817
|
+
"view": {"grep": pats[:1], "numbered": numbered,
|
|
818
|
+
"recursive": recursive}})
|
|
819
|
+
elif verb == "sed":
|
|
820
|
+
scripts, files, inplace, quiet, has_z, has_E = _sed_parts(argv)
|
|
821
|
+
files = [f for f in files if path_like(f)] + [t for o, t in u["redirs"] if o == "<"]
|
|
822
|
+
cmds = []
|
|
823
|
+
for s in scripts:
|
|
824
|
+
if s.startswith("__FILE__:"):
|
|
825
|
+
cmds.append({"cmd": "?", "raw": s})
|
|
826
|
+
else:
|
|
827
|
+
cmds.extend(parse_sed_script(s))
|
|
828
|
+
if inplace:
|
|
829
|
+
for f in files:
|
|
830
|
+
new_ops.append({"op": "W", "path": resolve(f, cwd), "kind": "anchored", "mech": "sed",
|
|
831
|
+
"anchor": {"type": "sed", "cmds": cmds, "z": has_z, "E": has_E, "raw": scripts}})
|
|
832
|
+
elif not swallowed:
|
|
833
|
+
rng = sed_view_range(cmds) if quiet else None
|
|
834
|
+
for f in files:
|
|
835
|
+
if rng:
|
|
836
|
+
view = {"range": list(rng)}
|
|
837
|
+
kind = "range"
|
|
838
|
+
elif not quiet and not any(c.get("cmd") in ("d",) for c in cmds):
|
|
839
|
+
view, kind = {"transformed": True}, "lines"
|
|
840
|
+
else:
|
|
841
|
+
view, kind = {"sed": True}, "lines"
|
|
842
|
+
if filtered(u):
|
|
843
|
+
kind, view = "lines", {"filtered": True, **view}
|
|
844
|
+
new_ops.append({"op": "R", "path": resolve(f, cwd), "kind": kind, "mech": "sed", "view": view})
|
|
845
|
+
elif verb in ("awk", "gawk", "mawk"):
|
|
846
|
+
inplace = "inplace" in argv
|
|
847
|
+
prog, files = None, []
|
|
848
|
+
skip = False
|
|
849
|
+
for i, a in enumerate(argv[1:], 1):
|
|
850
|
+
if skip:
|
|
851
|
+
skip = False
|
|
852
|
+
continue
|
|
853
|
+
if a in ("-f", "-v", "-i", "-F"):
|
|
854
|
+
skip = True
|
|
855
|
+
if a == "-f" and i + 1 < len(argv):
|
|
856
|
+
prog = "__FILE__"
|
|
857
|
+
continue
|
|
858
|
+
if a.startswith("-"):
|
|
859
|
+
continue
|
|
860
|
+
if prog is None:
|
|
861
|
+
prog = a
|
|
862
|
+
else:
|
|
863
|
+
files.append(a)
|
|
864
|
+
files = [f for f in files if path_like(f)]
|
|
865
|
+
if inplace:
|
|
866
|
+
for f in files:
|
|
867
|
+
new_ops.append({"op": "W", "path": resolve(f, cwd), "kind": "anchored", "mech": "awk",
|
|
868
|
+
"anchor": {"type": "awk", "program": prog}})
|
|
869
|
+
elif not swallowed:
|
|
870
|
+
for f in files:
|
|
871
|
+
new_ops.append({"op": "R", "path": resolve(f, cwd), "kind": "lines", "mech": "awk",
|
|
872
|
+
"view": {"awk": prog}})
|
|
873
|
+
elif verb == "perl":
|
|
874
|
+
files, script, skip, inplace, printing = [], None, False, False, False
|
|
875
|
+
for a in argv[1:]:
|
|
876
|
+
if skip:
|
|
877
|
+
script = a
|
|
878
|
+
skip = False
|
|
879
|
+
continue
|
|
880
|
+
if a.startswith("-") and not a.startswith("--"):
|
|
881
|
+
if "i" in a[1:]:
|
|
882
|
+
inplace = True
|
|
883
|
+
if set(a[1:]) & set("pn"):
|
|
884
|
+
printing = True
|
|
885
|
+
if a[-1] in "eE":
|
|
886
|
+
skip = True
|
|
887
|
+
continue
|
|
888
|
+
files.append(a)
|
|
889
|
+
if script is None and files:
|
|
890
|
+
script, files = files[0], files[1:]
|
|
891
|
+
files = [f for f in files if path_like(f)]
|
|
892
|
+
subs = []
|
|
893
|
+
for m in re.finditer(r"s(.)(.*?)(?<!\\)\1(.*?)(?<!\\)\1([a-z]*)", script or "", re.S):
|
|
894
|
+
subs.append({"pat": m.group(2), "rep": m.group(3), "flags": m.group(4)})
|
|
895
|
+
if inplace:
|
|
896
|
+
for f in files:
|
|
897
|
+
new_ops.append({"op": "W", "path": resolve(f, cwd), "kind": "anchored", "mech": "perl",
|
|
898
|
+
"anchor": {"type": "perl", "subs": subs, "script": script}})
|
|
899
|
+
elif printing and not swallowed:
|
|
900
|
+
for f in files:
|
|
901
|
+
new_ops.append({"op": "R", "path": resolve(f, cwd), "kind": "lines", "mech": "perl", "view": {}})
|
|
902
|
+
elif script:
|
|
903
|
+
w, r = python_script_ops("", cwd) # perl scripts: literal open() rarely used; skip
|
|
904
|
+
elif verb.startswith(INTERP_PREFIX) or verb in ("ipython",):
|
|
905
|
+
script = None
|
|
906
|
+
src = None
|
|
907
|
+
if "-c" in argv and argv.index("-c") + 1 < len(argv):
|
|
908
|
+
script = argv[argv.index("-c") + 1]
|
|
909
|
+
src = "-c"
|
|
910
|
+
elif u["heredoc"] is not None:
|
|
911
|
+
script = u["heredoc"]
|
|
912
|
+
src = "heredoc"
|
|
913
|
+
else:
|
|
914
|
+
for a in argv[1:]:
|
|
915
|
+
if a.startswith("-"):
|
|
916
|
+
if a == "-m":
|
|
917
|
+
break
|
|
918
|
+
continue
|
|
919
|
+
ap = resolve(a, cwd)
|
|
920
|
+
if ap in known_files:
|
|
921
|
+
script = known_files[ap]
|
|
922
|
+
src = "file:" + ap
|
|
923
|
+
break
|
|
924
|
+
if script is not None:
|
|
925
|
+
# a program run by path (not an inline edit script) counts as an
|
|
926
|
+
# anchored edit only when it performs literal text substitution
|
|
927
|
+
w, r = python_script_ops(script, cwd, strict=bool(src and src.startswith("file:")))
|
|
928
|
+
for o in w:
|
|
929
|
+
o["script_src"] = src
|
|
930
|
+
new_ops.extend(w)
|
|
931
|
+
elif verb in ("cp", "mv", "install"):
|
|
932
|
+
targets = [a for a in operands if path_like(a)]
|
|
933
|
+
if len(targets) >= 2:
|
|
934
|
+
dst = resolve(targets[-1], cwd)
|
|
935
|
+
for s in targets[:-1]:
|
|
936
|
+
sp = resolve(s, cwd)
|
|
937
|
+
d_ = dst
|
|
938
|
+
new_ops.append({"op": "W", "path": d_, "kind": "replace", "mech": verb, "src": sp,
|
|
939
|
+
"dst_may_be_dir": True})
|
|
940
|
+
if verb == "mv":
|
|
941
|
+
new_ops.append({"op": "W", "path": sp, "kind": "delete", "mech": "mv"})
|
|
942
|
+
elif verb == "rm":
|
|
943
|
+
for a in operands:
|
|
944
|
+
if path_like(a):
|
|
945
|
+
new_ops.append({"op": "W", "path": resolve(a, cwd), "kind": "delete", "mech": "rm"})
|
|
946
|
+
elif verb == "touch":
|
|
947
|
+
for a in operands:
|
|
948
|
+
if path_like(a):
|
|
949
|
+
new_ops.append({"op": "W", "path": resolve(a, cwd), "kind": "touch", "mech": "touch"})
|
|
950
|
+
elif verb == "tee":
|
|
951
|
+
app = "-a" in argv or "--append" in argv
|
|
952
|
+
for a in operands:
|
|
953
|
+
if path_like(a):
|
|
954
|
+
new_ops.append({"op": "W", "path": resolve(a, cwd),
|
|
955
|
+
"kind": "append" if app else ("overwrite" if literal else "replace"),
|
|
956
|
+
"mech": "tee", "text": _literal_text(units, u) if literal else None})
|
|
957
|
+
elif verb == "truncate":
|
|
958
|
+
for a in operands:
|
|
959
|
+
if path_like(a) and not re.fullmatch(r"\d+", a):
|
|
960
|
+
new_ops.append({"op": "W", "path": resolve(a, cwd), "kind": "replace", "mech": "truncate"})
|
|
961
|
+
elif verb in ("patch",):
|
|
962
|
+
body = u["heredoc"]
|
|
963
|
+
strip = 1
|
|
964
|
+
for a in argv[1:]:
|
|
965
|
+
m = re.fullmatch(r"-p(\d+)", a)
|
|
966
|
+
if m:
|
|
967
|
+
strip = int(m.group(1))
|
|
968
|
+
named = [a for a in operands if path_like(a) and not a.endswith((".patch", ".diff"))]
|
|
969
|
+
pfile = None
|
|
970
|
+
for op_, t in u["redirs"]:
|
|
971
|
+
if op_ == "<":
|
|
972
|
+
pfile = resolve(t, cwd)
|
|
973
|
+
for a in argv[1:]:
|
|
974
|
+
if a.startswith("-i") and len(a) > 2:
|
|
975
|
+
pfile = resolve(a[2:], cwd)
|
|
976
|
+
if body is None and pfile and pfile in known_files:
|
|
977
|
+
body = known_files[pfile]
|
|
978
|
+
files = parse_unified(body or "", cwd, strip)
|
|
979
|
+
if files:
|
|
980
|
+
for fobj in files:
|
|
981
|
+
new_ops.append({"op": "W", "path": fobj["path"], "kind": "anchored", "mech": "patch",
|
|
982
|
+
"anchor": {"type": "patch", "hunks": fobj["hunks"]}})
|
|
983
|
+
for a in named:
|
|
984
|
+
new_ops.append({"op": "W", "path": resolve(a, cwd), "kind": "anchored", "mech": "patch",
|
|
985
|
+
"anchor": {"type": "patch", "hunks": []}})
|
|
986
|
+
elif verb in ("applypatch", "apply_patch"):
|
|
987
|
+
files = parse_unified(u["heredoc"] or (argv[1] if len(argv) > 1 else ""), cwd, 0)
|
|
988
|
+
for fobj in files:
|
|
989
|
+
new_ops.append({"op": "W", "path": fobj["path"], "kind": "anchored", "mech": "applypatch",
|
|
990
|
+
"anchor": {"type": "patch", "hunks": fobj["hunks"]}})
|
|
991
|
+
elif verb in ("ed", "ex"):
|
|
992
|
+
files = [a for a in operands if path_like(a)]
|
|
993
|
+
for f in files:
|
|
994
|
+
new_ops.append({"op": "W", "path": resolve(f, cwd), "kind": "anchored", "mech": "ed",
|
|
995
|
+
"anchor": {"type": "ed", "script": u["heredoc"] or ""}})
|
|
996
|
+
elif verb == "git" and operands:
|
|
997
|
+
sub = operands[0]
|
|
998
|
+
rest = operands[1:]
|
|
999
|
+
after_dd = argv[argv.index("--") + 1:] if "--" in argv else None
|
|
1000
|
+
if sub == "apply":
|
|
1001
|
+
body = u["heredoc"]
|
|
1002
|
+
pf = [a for a in rest if path_like(a)]
|
|
1003
|
+
if body is None and pf and resolve(pf[0], cwd) in known_files:
|
|
1004
|
+
body = known_files[resolve(pf[0], cwd)]
|
|
1005
|
+
for fobj in parse_unified(body or "", cwd, 1):
|
|
1006
|
+
new_ops.append({"op": "W", "path": fobj["path"], "kind": "anchored", "mech": "git-apply",
|
|
1007
|
+
"anchor": {"type": "patch", "hunks": fobj["hunks"]}})
|
|
1008
|
+
elif sub in ("checkout", "restore"):
|
|
1009
|
+
named = after_dd if after_dd is not None else [r for r in rest if path_like(r) or r == "."]
|
|
1010
|
+
if "." in named or not named:
|
|
1011
|
+
if sub == "checkout" and rest and not after_dd and not any(path_like(r) for r in rest):
|
|
1012
|
+
new_ops.append({"op": "W", "path": "*", "kind": "restore", "mech": "git-checkout-branch"})
|
|
1013
|
+
else:
|
|
1014
|
+
new_ops.append({"op": "W", "path": "*", "kind": "restore", "mech": "git"})
|
|
1015
|
+
else:
|
|
1016
|
+
for p in named:
|
|
1017
|
+
new_ops.append({"op": "W", "path": resolve(p, cwd), "kind": "restore", "mech": "git"})
|
|
1018
|
+
elif sub == "stash" and (not rest or rest[0] in ("push", "save")):
|
|
1019
|
+
new_ops.append({"op": "W", "path": "*", "kind": "restore", "mech": "git-stash"})
|
|
1020
|
+
elif sub == "stash" and rest and rest[0] in ("pop", "apply"):
|
|
1021
|
+
new_ops.append({"op": "W", "path": "*", "kind": "replace", "mech": "git-stash-pop"})
|
|
1022
|
+
elif sub == "reset" and "--hard" in argv:
|
|
1023
|
+
new_ops.append({"op": "W", "path": "*", "kind": "restore", "mech": "git-reset"})
|
|
1024
|
+
elif sub in ("diff", "show") and not swallowed:
|
|
1025
|
+
named = after_dd if after_dd is not None else [r for r in rest if path_like(r) and "/" in r or
|
|
1026
|
+
re.search(r"\.[A-Za-z]{1,6}$", r or "")]
|
|
1027
|
+
for p in named:
|
|
1028
|
+
new_ops.append({"op": "R", "path": resolve(p, cwd), "kind": "lines", "mech": "git-diff",
|
|
1029
|
+
"view": {"diff": True}})
|
|
1030
|
+
elif verb == "str_replace_editor":
|
|
1031
|
+
pass # handled by the SWE-agent adapter
|
|
1032
|
+
elif verb in ("bash", "sh", "source", ".") and _DEPTH[0] < 2:
|
|
1033
|
+
# a shell script the agent wrote earlier in the trajectory: its
|
|
1034
|
+
# commands are the agent's commands
|
|
1035
|
+
script = None
|
|
1036
|
+
if "-c" in argv and argv.index("-c") + 1 < len(argv):
|
|
1037
|
+
script = argv[argv.index("-c") + 1]
|
|
1038
|
+
else:
|
|
1039
|
+
for a in argv[1:]:
|
|
1040
|
+
if not a.startswith("-"):
|
|
1041
|
+
script = known_files.get(resolve(a, cwd))
|
|
1042
|
+
break
|
|
1043
|
+
if script is None and u["heredoc"] is not None:
|
|
1044
|
+
script = u["heredoc"]
|
|
1045
|
+
if script:
|
|
1046
|
+
_DEPTH[0] += 1
|
|
1047
|
+
try:
|
|
1048
|
+
sub_ops, _, _ = extract(script, cwd, known_files, persist_cd=False)
|
|
1049
|
+
finally:
|
|
1050
|
+
_DEPTH[0] -= 1
|
|
1051
|
+
for so in sub_ops:
|
|
1052
|
+
so["via_script"] = True
|
|
1053
|
+
new_ops.extend(sub_ops)
|
|
1054
|
+
|
|
1055
|
+
# output redirections of this unit
|
|
1056
|
+
for op_, t in u["redirs"]:
|
|
1057
|
+
if not path_like(t) and not t.startswith("/tmp"):
|
|
1058
|
+
if not t.startswith("/"):
|
|
1059
|
+
continue
|
|
1060
|
+
if t.startswith("/dev/"):
|
|
1061
|
+
continue
|
|
1062
|
+
if op_ in (">", ">|", "&>", "1>"):
|
|
1063
|
+
text = _literal_text(units, u) if literal else None
|
|
1064
|
+
new_ops.append({"op": "W", "path": resolve(t, cwd), "kind": "overwrite" if literal else "replace",
|
|
1065
|
+
"mech": "redirect", "text": text,
|
|
1066
|
+
"derived_from": [o["path"] for o in new_ops if o["op"] == "R" or o.get("_src")]})
|
|
1067
|
+
elif op_ in (">>", "&>>", "1>>"):
|
|
1068
|
+
new_ops.append({"op": "W", "path": resolve(t, cwd), "kind": "append", "mech": "redirect",
|
|
1069
|
+
"text": _literal_text(units, u) if literal else None})
|
|
1070
|
+
# remember heredoc-created files (scripts, patches)
|
|
1071
|
+
if u["heredoc"] is not None:
|
|
1072
|
+
for o in new_ops:
|
|
1073
|
+
if o["op"] == "W" and o["kind"] == "overwrite" and o["mech"] in ("redirect", "tee"):
|
|
1074
|
+
known_files[o["path"]] = u["heredoc"]
|
|
1075
|
+
for o in new_ops:
|
|
1076
|
+
o["cmd_i"] = ui
|
|
1077
|
+
o["guard"] = guard
|
|
1078
|
+
if o.get("via_script"):
|
|
1079
|
+
# keep the inner command's verb (e.g. `ed` inside `bash x.sh`),
|
|
1080
|
+
# so "ed: command not found" marks it failed
|
|
1081
|
+
o["outer_verb"] = verb
|
|
1082
|
+
if o.get("verb") is None:
|
|
1083
|
+
o["verb"] = verb
|
|
1084
|
+
else:
|
|
1085
|
+
o["verb"] = verb
|
|
1086
|
+
ops.extend(new_ops)
|
|
1087
|
+
if not persist_cd:
|
|
1088
|
+
cwd = cwd0
|
|
1089
|
+
return ops, cwd, info
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _literal_text(units, u) -> Optional[str]:
|
|
1093
|
+
"""The literal text flowing into u's pipe (echo/printf args or a heredoc)."""
|
|
1094
|
+
for v in units:
|
|
1095
|
+
if v["pipe"] != u["pipe"]:
|
|
1096
|
+
continue
|
|
1097
|
+
argv = strip_prefixes(v["argv"])
|
|
1098
|
+
verb = argv[0].split("/")[-1] if argv else ""
|
|
1099
|
+
if verb == "cat" and v["heredoc"] is not None:
|
|
1100
|
+
return v["heredoc"] + "\n"
|
|
1101
|
+
if verb == "echo":
|
|
1102
|
+
args = [a for a in argv[1:] if a not in ("-e", "-n", "-E")]
|
|
1103
|
+
return " ".join(args) + ("" if "-n" in argv else "\n")
|
|
1104
|
+
if verb == "printf":
|
|
1105
|
+
return " ".join(argv[1:2]).replace("\\n", "\n")
|
|
1106
|
+
if v["heredoc"] is not None and verb in ("tee",):
|
|
1107
|
+
return v["heredoc"] + "\n"
|
|
1108
|
+
return None
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _selftest():
|
|
1112
|
+
cases = [
|
|
1113
|
+
("cd src && sed -i 's/a/b/' x.py", "/repo"),
|
|
1114
|
+
("sed -n '10,20p' a.py", "/repo"),
|
|
1115
|
+
("sed 's/a/b/' a.py > /tmp/o", "/repo"),
|
|
1116
|
+
("cat <<'EOF' > fix.py\nimport re\np='a.py'\ns=open(p).read()\nopen(p,'w').write(s.replace('x = 1','x = 2'))\nEOF\npython3 fix.py", "/repo"),
|
|
1117
|
+
("grep -n foo a.py | head", "/repo"),
|
|
1118
|
+
("grep -c foo a.py", "/repo"),
|
|
1119
|
+
("nl -ba a.py | sed -n '1,40p'", "/repo"),
|
|
1120
|
+
("sed -i '42d;45s/foo/bar/' Makefile", "/repo"),
|
|
1121
|
+
("sed -i '/pattern/a\\ new line' a.py", "/repo"),
|
|
1122
|
+
("perl -0pi -e 's/foo\\n/bar/' a.py", "/repo"),
|
|
1123
|
+
("python3 - <<'PY'\nfrom pathlib import Path\np=Path('a.py')\ns=p.read_text()\np.write_text(s.replace('a','b'))\nPY", "/repo"),
|
|
1124
|
+
("applypatch << 'PATCH'\n*** Begin Patch\n*** Update File: a.py\n@@\n-x = 1\n+x = 2\n*** End Patch\nPATCH", "/repo"),
|
|
1125
|
+
("(cd sub && cat f.txt) && cat g.txt", "/repo"),
|
|
1126
|
+
("head -n 50 a.py", "/repo"),
|
|
1127
|
+
("echo 'x' >> a.py && cat a.py | wc -l", "/repo"),
|
|
1128
|
+
("git checkout -- a.py b.py", "/repo"),
|
|
1129
|
+
("cp a.py a.py.bak && sed -i 's/x/y/' a.py", "/repo"),
|
|
1130
|
+
]
|
|
1131
|
+
kf: Dict[str, str] = {}
|
|
1132
|
+
for c, cwd in cases:
|
|
1133
|
+
ops, cwd2, info = extract(c, cwd, kf)
|
|
1134
|
+
print(repr(c[:70]))
|
|
1135
|
+
for o in ops:
|
|
1136
|
+
print(" ", {k: v for k, v in o.items() if k not in ("guard",)})
|
|
1137
|
+
print(" cwd->", cwd2)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
if __name__ == "__main__":
|
|
1141
|
+
_selftest()
|