codetool-shell 0.1.1__py3-none-win_amd64.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.
- codetool_shell/__init__.py +11 -0
- codetool_shell/api.py +59 -0
- codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
- codetool_shell/filters/__init__.py +14 -0
- codetool_shell/filters/build_compiler/__init__.py +7 -0
- codetool_shell/filters/build_compiler/detector.py +412 -0
- codetool_shell/filters/build_compiler/reducer.py +166 -0
- codetool_shell/filters/build_compiler/summary.py +617 -0
- codetool_shell/filters/ci_job_log/__init__.py +7 -0
- codetool_shell/filters/ci_job_log/detector.py +64 -0
- codetool_shell/filters/ci_job_log/reducer.py +99 -0
- codetool_shell/filters/ci_job_log/summary.py +243 -0
- codetool_shell/filters/diff/__init__.py +7 -0
- codetool_shell/filters/diff/detector.py +136 -0
- codetool_shell/filters/diff/reducer.py +308 -0
- codetool_shell/filters/generic_log/__init__.py +7 -0
- codetool_shell/filters/generic_log/detector.py +175 -0
- codetool_shell/filters/generic_log/reducer.py +99 -0
- codetool_shell/filters/generic_log/summary.py +161 -0
- codetool_shell/filters/git.py +514 -0
- codetool_shell/filters/html_cleanup/__init__.py +7 -0
- codetool_shell/filters/html_cleanup/detector.py +136 -0
- codetool_shell/filters/html_cleanup/reducer.py +27 -0
- codetool_shell/filters/html_cleanup/summary.py +422 -0
- codetool_shell/filters/json_payload/__init__.py +7 -0
- codetool_shell/filters/json_payload/detector.py +62 -0
- codetool_shell/filters/json_payload/reducer.py +81 -0
- codetool_shell/filters/json_payload/summary.py +233 -0
- codetool_shell/filters/listing/__init__.py +7 -0
- codetool_shell/filters/listing/detector.py +294 -0
- codetool_shell/filters/listing/reducer.py +30 -0
- codetool_shell/filters/log_template/__init__.py +7 -0
- codetool_shell/filters/log_template/constants.py +76 -0
- codetool_shell/filters/log_template/detector.py +331 -0
- codetool_shell/filters/log_template/reducer.py +78 -0
- codetool_shell/filters/log_template/template.py +280 -0
- codetool_shell/filters/log_template/types.py +21 -0
- codetool_shell/filters/opaque_payload/__init__.py +7 -0
- codetool_shell/filters/opaque_payload/detector.py +563 -0
- codetool_shell/filters/opaque_payload/reducer.py +142 -0
- codetool_shell/filters/opaque_payload/summary.py +61 -0
- codetool_shell/filters/package_manager/__init__.py +7 -0
- codetool_shell/filters/package_manager/detector.py +220 -0
- codetool_shell/filters/package_manager/reducer.py +110 -0
- codetool_shell/filters/package_manager/summary.py +172 -0
- codetool_shell/filters/pipeline.py +65 -0
- codetool_shell/filters/rg.py +250 -0
- codetool_shell/filters/system_output/__init__.py +7 -0
- codetool_shell/filters/system_output/detector.py +600 -0
- codetool_shell/filters/system_output/reducer.py +331 -0
- codetool_shell/filters/system_output/summary.py +164 -0
- codetool_shell/filters/table/__init__.py +7 -0
- codetool_shell/filters/table/detector.py +244 -0
- codetool_shell/filters/table/reducer.py +57 -0
- codetool_shell/filters/table/summary.py +37 -0
- codetool_shell/filters/test_runner/__init__.py +7 -0
- codetool_shell/filters/test_runner/ansi.py +80 -0
- codetool_shell/filters/test_runner/detector.py +409 -0
- codetool_shell/filters/test_runner/reducer.py +288 -0
- codetool_shell/filters/test_runner/summary.py +449 -0
- codetool_shell/filters/text.py +38 -0
- codetool_shell/filters/traceback/__init__.py +7 -0
- codetool_shell/filters/traceback/detector.py +209 -0
- codetool_shell/filters/traceback/reducer.py +141 -0
- codetool_shell/filters/traceback/summary.py +122 -0
- codetool_shell/filters/tree.py +59 -0
- codetool_shell/py.typed +0 -0
- codetool_shell/python_backend.py +38 -0
- codetool_shell/rust_backend.py +254 -0
- codetool_shell-0.1.1.dist-info/METADATA +152 -0
- codetool_shell-0.1.1.dist-info/RECORD +72 -0
- codetool_shell-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""Conservative detectors for common system command output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class EnvVar:
|
|
10
|
+
key: str
|
|
11
|
+
value: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class EnvOutput:
|
|
16
|
+
vars: tuple[EnvVar, ...]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DfRow:
|
|
21
|
+
line: str
|
|
22
|
+
usage: int
|
|
23
|
+
mount: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DfOutput:
|
|
28
|
+
header: str
|
|
29
|
+
rows: tuple[DfRow, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class DuRow:
|
|
34
|
+
line: str
|
|
35
|
+
size: float
|
|
36
|
+
path: str
|
|
37
|
+
is_total: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class DuOutput:
|
|
42
|
+
rows: tuple[DuRow, ...]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class PsRow:
|
|
47
|
+
line: str
|
|
48
|
+
cpu: float | None
|
|
49
|
+
mem: float | None
|
|
50
|
+
command: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class PsOutput:
|
|
55
|
+
header: str
|
|
56
|
+
rows: tuple[PsRow, ...]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class PingOutput:
|
|
61
|
+
lines: tuple[str, ...]
|
|
62
|
+
reply_indices: tuple[int, ...]
|
|
63
|
+
event_indices: tuple[int, ...]
|
|
64
|
+
summary_indices: tuple[int, ...]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class SystemctlStatusOutput:
|
|
69
|
+
lines: tuple[str, ...]
|
|
70
|
+
core_indices: tuple[int, ...]
|
|
71
|
+
alert_indices: tuple[int, ...]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class WcRow:
|
|
76
|
+
line: str
|
|
77
|
+
numbers: tuple[int, ...]
|
|
78
|
+
path: str
|
|
79
|
+
is_total: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class WcOutput:
|
|
84
|
+
rows: tuple[WcRow, ...]
|
|
85
|
+
total_index: int
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class StatBlock:
|
|
90
|
+
file: str
|
|
91
|
+
size: str
|
|
92
|
+
mode: str
|
|
93
|
+
owner: str
|
|
94
|
+
group: str
|
|
95
|
+
modify: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_MIN_ENV_VARS = 12
|
|
99
|
+
_MIN_DF_ROWS = 8
|
|
100
|
+
_MIN_DU_ROWS = 12
|
|
101
|
+
_MIN_PS_ROWS = 10
|
|
102
|
+
_MIN_PING_REPLIES = 8
|
|
103
|
+
_MIN_SYSTEMCTL_LINES = 10
|
|
104
|
+
_MIN_WC_FILE_ROWS = 8
|
|
105
|
+
_MIN_STAT_BLOCKS = 3
|
|
106
|
+
|
|
107
|
+
_ENV_ANCHOR_KEYS = frozenset(
|
|
108
|
+
{
|
|
109
|
+
"HOME",
|
|
110
|
+
"PATH",
|
|
111
|
+
"PWD",
|
|
112
|
+
"SHELL",
|
|
113
|
+
"USER",
|
|
114
|
+
"LOGNAME",
|
|
115
|
+
"TERM",
|
|
116
|
+
"LANG",
|
|
117
|
+
"VIRTUAL_ENV",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
_PS_COMMAND_HEADERS = frozenset({"COMMAND", "CMD", "ARGS"})
|
|
122
|
+
_SIZE_UNITS = {
|
|
123
|
+
"": 1.0,
|
|
124
|
+
"B": 1.0,
|
|
125
|
+
"K": 1024.0,
|
|
126
|
+
"KB": 1024.0,
|
|
127
|
+
"KI": 1024.0,
|
|
128
|
+
"KIB": 1024.0,
|
|
129
|
+
"M": 1024.0**2,
|
|
130
|
+
"MB": 1024.0**2,
|
|
131
|
+
"MI": 1024.0**2,
|
|
132
|
+
"MIB": 1024.0**2,
|
|
133
|
+
"G": 1024.0**3,
|
|
134
|
+
"GB": 1024.0**3,
|
|
135
|
+
"GI": 1024.0**3,
|
|
136
|
+
"GIB": 1024.0**3,
|
|
137
|
+
"T": 1024.0**4,
|
|
138
|
+
"TB": 1024.0**4,
|
|
139
|
+
"TI": 1024.0**4,
|
|
140
|
+
"TIB": 1024.0**4,
|
|
141
|
+
"P": 1024.0**5,
|
|
142
|
+
"PB": 1024.0**5,
|
|
143
|
+
"PI": 1024.0**5,
|
|
144
|
+
"PIB": 1024.0**5,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_env_output(lines: list[str]) -> EnvOutput | None:
|
|
149
|
+
if len(lines) < _MIN_ENV_VARS or any(not line for line in lines):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
vars_: list[EnvVar] = []
|
|
153
|
+
uppercase_count = 0
|
|
154
|
+
for line in lines:
|
|
155
|
+
parsed = _parse_env_line(line)
|
|
156
|
+
if parsed is None:
|
|
157
|
+
return None
|
|
158
|
+
vars_.append(parsed)
|
|
159
|
+
if parsed.key.upper() == parsed.key:
|
|
160
|
+
uppercase_count += 1
|
|
161
|
+
|
|
162
|
+
keys = {var.key for var in vars_}
|
|
163
|
+
if not keys.intersection(_ENV_ANCHOR_KEYS) and uppercase_count * 2 < len(vars_):
|
|
164
|
+
return None
|
|
165
|
+
return EnvOutput(tuple(vars_))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def parse_df_output(lines: list[str]) -> DfOutput | None:
|
|
169
|
+
if len(lines) < _MIN_DF_ROWS + 1 or not lines:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
header = lines[0]
|
|
173
|
+
header_tokens = header.split()
|
|
174
|
+
if not header_tokens or header_tokens[0] != "Filesystem":
|
|
175
|
+
return None
|
|
176
|
+
use_index = _find_use_percent_index(header_tokens)
|
|
177
|
+
if use_index is None or "Mounted" not in header_tokens:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
rows: list[DfRow] = []
|
|
181
|
+
for line in lines[1:]:
|
|
182
|
+
if not line.strip():
|
|
183
|
+
return None
|
|
184
|
+
row = _parse_df_row(line, use_index)
|
|
185
|
+
if row is None:
|
|
186
|
+
return None
|
|
187
|
+
rows.append(row)
|
|
188
|
+
|
|
189
|
+
if len(rows) < _MIN_DF_ROWS:
|
|
190
|
+
return None
|
|
191
|
+
return DfOutput(header, tuple(rows))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_du_output(lines: list[str]) -> DuOutput | None:
|
|
195
|
+
if len(lines) < _MIN_DU_ROWS or any(not line.strip() for line in lines):
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
rows: list[DuRow] = []
|
|
199
|
+
path_like = 0
|
|
200
|
+
unitless = 0
|
|
201
|
+
file_like = 0
|
|
202
|
+
for line in lines:
|
|
203
|
+
row = _parse_du_row(line)
|
|
204
|
+
if row is None:
|
|
205
|
+
return None
|
|
206
|
+
rows.append(row)
|
|
207
|
+
if row.is_total or _looks_like_path(row.path):
|
|
208
|
+
path_like += 1
|
|
209
|
+
if _is_unitless_size_token(line.split(maxsplit=1)[0]):
|
|
210
|
+
unitless += 1
|
|
211
|
+
if not row.is_total and "." in row.path.rsplit("/", 1)[-1]:
|
|
212
|
+
file_like += 1
|
|
213
|
+
|
|
214
|
+
if path_like * 2 < len(rows):
|
|
215
|
+
return None
|
|
216
|
+
non_total = len(rows) - sum(row.is_total for row in rows)
|
|
217
|
+
if unitless == len(rows) and non_total > 0 and file_like * 2 >= non_total:
|
|
218
|
+
return None
|
|
219
|
+
return DuOutput(tuple(rows))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def parse_ps_output(lines: list[str]) -> PsOutput | None:
|
|
223
|
+
if len(lines) < _MIN_PS_ROWS + 1:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
header = lines[0]
|
|
227
|
+
header_tokens = header.split()
|
|
228
|
+
if "PID" not in header_tokens:
|
|
229
|
+
return None
|
|
230
|
+
command_index = _find_command_index(header_tokens)
|
|
231
|
+
if command_index is None:
|
|
232
|
+
return None
|
|
233
|
+
cpu_index = _find_optional_index(header_tokens, "%CPU")
|
|
234
|
+
mem_index = _find_optional_index(header_tokens, "%MEM")
|
|
235
|
+
if cpu_index is None and mem_index is None:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
pid_index = header_tokens.index("PID")
|
|
239
|
+
rows: list[PsRow] = []
|
|
240
|
+
for line in lines[1:]:
|
|
241
|
+
if not line.strip():
|
|
242
|
+
return None
|
|
243
|
+
parts = line.split(maxsplit=len(header_tokens) - 1)
|
|
244
|
+
if len(parts) < len(header_tokens) or not parts[pid_index].isdigit():
|
|
245
|
+
return None
|
|
246
|
+
cpu = _parse_float_at(parts, cpu_index)
|
|
247
|
+
mem = _parse_float_at(parts, mem_index)
|
|
248
|
+
command = parts[command_index]
|
|
249
|
+
rows.append(PsRow(line=line, cpu=cpu, mem=mem, command=command))
|
|
250
|
+
|
|
251
|
+
if len(rows) < _MIN_PS_ROWS:
|
|
252
|
+
return None
|
|
253
|
+
return PsOutput(header, tuple(rows))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def parse_ping_output(lines: list[str]) -> PingOutput | None:
|
|
257
|
+
if not lines or not lines[0].startswith("PING "):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
reply_indices: list[int] = []
|
|
261
|
+
event_indices: list[int] = []
|
|
262
|
+
summary_indices: list[int] = []
|
|
263
|
+
for index, line in enumerate(lines):
|
|
264
|
+
lower = line.lower()
|
|
265
|
+
if index == 0:
|
|
266
|
+
continue
|
|
267
|
+
if _is_ping_reply(lower):
|
|
268
|
+
reply_indices.append(index)
|
|
269
|
+
elif _is_ping_summary_line(lower):
|
|
270
|
+
summary_indices.append(index)
|
|
271
|
+
elif _is_ping_event_line(lower):
|
|
272
|
+
event_indices.append(index)
|
|
273
|
+
|
|
274
|
+
if len(reply_indices) < _MIN_PING_REPLIES and len(lines) < _MIN_PING_REPLIES + 4:
|
|
275
|
+
return None
|
|
276
|
+
if not summary_indices and not event_indices:
|
|
277
|
+
return None
|
|
278
|
+
return PingOutput(
|
|
279
|
+
lines=tuple(lines),
|
|
280
|
+
reply_indices=tuple(reply_indices),
|
|
281
|
+
event_indices=tuple(event_indices),
|
|
282
|
+
summary_indices=tuple(summary_indices),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def parse_systemctl_status_output(lines: list[str]) -> SystemctlStatusOutput | None:
|
|
287
|
+
if len(lines) < _MIN_SYSTEMCTL_LINES:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
stripped = [line.strip() for line in lines]
|
|
291
|
+
has_unit_header = stripped[0].startswith("● ") or ".service" in stripped[0]
|
|
292
|
+
has_loaded = any(line.startswith("Loaded:") for line in stripped)
|
|
293
|
+
has_active = any(line.startswith("Active:") for line in stripped)
|
|
294
|
+
if not (has_unit_header and has_loaded and has_active):
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
core_indices: list[int] = [0]
|
|
298
|
+
alert_indices: list[int] = []
|
|
299
|
+
for index, line in enumerate(stripped):
|
|
300
|
+
if index == 0:
|
|
301
|
+
continue
|
|
302
|
+
if line.startswith(("Loaded:", "Active:", "Main PID:")):
|
|
303
|
+
core_indices.append(index)
|
|
304
|
+
if _contains_alert(line):
|
|
305
|
+
alert_indices.append(index)
|
|
306
|
+
|
|
307
|
+
if len(core_indices) < 3:
|
|
308
|
+
return None
|
|
309
|
+
return SystemctlStatusOutput(
|
|
310
|
+
lines=tuple(lines),
|
|
311
|
+
core_indices=tuple(_unique_sorted(core_indices)),
|
|
312
|
+
alert_indices=tuple(_unique_sorted(alert_indices)),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def parse_wc_output(lines: list[str]) -> WcOutput | None:
|
|
317
|
+
if len(lines) < _MIN_WC_FILE_ROWS + 1 or any(not line.strip() for line in lines):
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
rows: list[WcRow] = []
|
|
321
|
+
numeric_columns: int | None = None
|
|
322
|
+
total_index: int | None = None
|
|
323
|
+
for index, line in enumerate(lines):
|
|
324
|
+
row = _parse_wc_row(line)
|
|
325
|
+
if row is None:
|
|
326
|
+
return None
|
|
327
|
+
if numeric_columns is None:
|
|
328
|
+
numeric_columns = len(row.numbers)
|
|
329
|
+
elif len(row.numbers) != numeric_columns:
|
|
330
|
+
return None
|
|
331
|
+
if row.is_total:
|
|
332
|
+
total_index = index
|
|
333
|
+
rows.append(row)
|
|
334
|
+
|
|
335
|
+
if total_index is None:
|
|
336
|
+
return None
|
|
337
|
+
file_rows = [row for row in rows if not row.is_total]
|
|
338
|
+
if len(file_rows) < _MIN_WC_FILE_ROWS:
|
|
339
|
+
return None
|
|
340
|
+
if numeric_columns == 1 and sum(_looks_like_file_path(row.path) for row in file_rows) * 2 < len(file_rows):
|
|
341
|
+
return None
|
|
342
|
+
return WcOutput(tuple(rows), total_index)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def parse_stat_blocks(lines: list[str]) -> tuple[StatBlock, ...] | None:
|
|
346
|
+
if len(lines) < _MIN_STAT_BLOCKS * 5:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
blocks: list[list[str]] = []
|
|
350
|
+
current: list[str] = []
|
|
351
|
+
for line in lines:
|
|
352
|
+
if line.strip().startswith("File:"):
|
|
353
|
+
if current:
|
|
354
|
+
blocks.append(current)
|
|
355
|
+
current = [line]
|
|
356
|
+
elif current:
|
|
357
|
+
current.append(line)
|
|
358
|
+
elif line.strip():
|
|
359
|
+
return None
|
|
360
|
+
if current:
|
|
361
|
+
blocks.append(current)
|
|
362
|
+
|
|
363
|
+
if len(blocks) < _MIN_STAT_BLOCKS:
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
parsed: list[StatBlock] = []
|
|
367
|
+
for block in blocks:
|
|
368
|
+
stat_block = _parse_stat_block(block)
|
|
369
|
+
if stat_block is None:
|
|
370
|
+
return None
|
|
371
|
+
parsed.append(stat_block)
|
|
372
|
+
return tuple(parsed)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _parse_env_line(line: str) -> EnvVar | None:
|
|
376
|
+
if "=" not in line or line.startswith(("export ", "declare ")):
|
|
377
|
+
return None
|
|
378
|
+
key, value = line.split("=", 1)
|
|
379
|
+
if not key or not (key[0].isalpha() or key[0] == "_"):
|
|
380
|
+
return None
|
|
381
|
+
if any(not (char.isalnum() or char == "_") for char in key):
|
|
382
|
+
return None
|
|
383
|
+
return EnvVar(key, value)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _find_use_percent_index(tokens: list[str]) -> int | None:
|
|
387
|
+
for index, token in enumerate(tokens):
|
|
388
|
+
if token.endswith("Use%"):
|
|
389
|
+
return index
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _parse_df_row(line: str, use_index: int) -> DfRow | None:
|
|
394
|
+
tokens = line.split()
|
|
395
|
+
if len(tokens) <= use_index + 1:
|
|
396
|
+
return None
|
|
397
|
+
usage_token = tokens[use_index]
|
|
398
|
+
if not usage_token.endswith("%") or not usage_token[:-1].isdigit():
|
|
399
|
+
return None
|
|
400
|
+
mount = " ".join(tokens[use_index + 1 :])
|
|
401
|
+
if not mount:
|
|
402
|
+
return None
|
|
403
|
+
return DfRow(line=line, usage=int(usage_token[:-1]), mount=mount)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _parse_du_row(line: str) -> DuRow | None:
|
|
407
|
+
parts = line.split(maxsplit=1)
|
|
408
|
+
if len(parts) != 2:
|
|
409
|
+
return None
|
|
410
|
+
size = _parse_size(parts[0])
|
|
411
|
+
if size is None:
|
|
412
|
+
return None
|
|
413
|
+
path = parts[1].strip()
|
|
414
|
+
if not path:
|
|
415
|
+
return None
|
|
416
|
+
return DuRow(line=line, size=size, path=path, is_total=path == "total")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _parse_size(token: str) -> float | None:
|
|
420
|
+
number = ""
|
|
421
|
+
unit = ""
|
|
422
|
+
for char in token:
|
|
423
|
+
if char.isdigit() or char == ".":
|
|
424
|
+
number += char
|
|
425
|
+
else:
|
|
426
|
+
unit += char
|
|
427
|
+
if not number or number.count(".") > 1:
|
|
428
|
+
return None
|
|
429
|
+
unit = unit.upper()
|
|
430
|
+
multiplier = _SIZE_UNITS.get(unit)
|
|
431
|
+
if multiplier is None:
|
|
432
|
+
return None
|
|
433
|
+
try:
|
|
434
|
+
return float(number) * multiplier
|
|
435
|
+
except ValueError:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _is_unitless_size_token(token: str) -> bool:
|
|
440
|
+
return token.isdigit()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _find_command_index(tokens: list[str]) -> int | None:
|
|
444
|
+
for index, token in enumerate(tokens):
|
|
445
|
+
if token in _PS_COMMAND_HEADERS:
|
|
446
|
+
return index
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _find_optional_index(tokens: list[str], value: str) -> int | None:
|
|
451
|
+
try:
|
|
452
|
+
return tokens.index(value)
|
|
453
|
+
except ValueError:
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _parse_float_at(parts: list[str], index: int | None) -> float | None:
|
|
458
|
+
if index is None or index >= len(parts):
|
|
459
|
+
return None
|
|
460
|
+
try:
|
|
461
|
+
return float(parts[index])
|
|
462
|
+
except ValueError:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _is_ping_reply(lower: str) -> bool:
|
|
467
|
+
return "bytes from" in lower and ("icmp_seq" in lower or "icmp_seq=" in lower)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _is_ping_summary_line(lower: str) -> bool:
|
|
471
|
+
return (
|
|
472
|
+
lower.startswith("--- ")
|
|
473
|
+
or "packets transmitted" in lower
|
|
474
|
+
or lower.startswith("rtt ")
|
|
475
|
+
or lower.startswith("round-trip ")
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _is_ping_event_line(lower: str) -> bool:
|
|
480
|
+
return any(
|
|
481
|
+
marker in lower
|
|
482
|
+
for marker in (
|
|
483
|
+
"timeout",
|
|
484
|
+
"unreachable",
|
|
485
|
+
"unknown host",
|
|
486
|
+
"name or service",
|
|
487
|
+
"temporary failure",
|
|
488
|
+
"packet loss",
|
|
489
|
+
"error",
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _contains_alert(line: str) -> bool:
|
|
495
|
+
lower = line.lower()
|
|
496
|
+
return any(
|
|
497
|
+
word in lower
|
|
498
|
+
for word in (
|
|
499
|
+
"failed",
|
|
500
|
+
"failure",
|
|
501
|
+
"error",
|
|
502
|
+
"warning",
|
|
503
|
+
"warn",
|
|
504
|
+
"critical",
|
|
505
|
+
"denied",
|
|
506
|
+
"refused",
|
|
507
|
+
"timeout",
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _parse_wc_row(line: str) -> WcRow | None:
|
|
513
|
+
tokens = line.split()
|
|
514
|
+
if len(tokens) < 2:
|
|
515
|
+
return None
|
|
516
|
+
numbers: list[int] = []
|
|
517
|
+
cursor = 0
|
|
518
|
+
for token in tokens:
|
|
519
|
+
if token.isdigit():
|
|
520
|
+
numbers.append(int(token))
|
|
521
|
+
cursor += 1
|
|
522
|
+
else:
|
|
523
|
+
break
|
|
524
|
+
if not numbers or cursor >= len(tokens):
|
|
525
|
+
return None
|
|
526
|
+
path = " ".join(tokens[cursor:])
|
|
527
|
+
return WcRow(line=line, numbers=tuple(numbers), path=path, is_total=path == "total")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _parse_stat_block(lines: list[str]) -> StatBlock | None:
|
|
531
|
+
file_name = _value_after_label(lines[0], "File:")
|
|
532
|
+
size = mode = owner = group = modify = None
|
|
533
|
+
for line in lines[1:]:
|
|
534
|
+
stripped = line.strip()
|
|
535
|
+
if stripped.startswith("Size:"):
|
|
536
|
+
size = _field_between(stripped, "Size:", "Blocks:") or _value_after_label(stripped, "Size:")
|
|
537
|
+
elif stripped.startswith("Access: (") and "Uid:" in stripped and "Gid:" in stripped:
|
|
538
|
+
mode = _paren_after(stripped, "Access:")
|
|
539
|
+
owner = _paren_after(stripped, "Uid:")
|
|
540
|
+
group = _paren_after(stripped, "Gid:")
|
|
541
|
+
elif stripped.startswith("Modify:"):
|
|
542
|
+
modify = _value_after_label(stripped, "Modify:")
|
|
543
|
+
|
|
544
|
+
if not all((file_name, size, mode, owner, group, modify)):
|
|
545
|
+
return None
|
|
546
|
+
return StatBlock(
|
|
547
|
+
file=file_name.strip(),
|
|
548
|
+
size=size.strip(),
|
|
549
|
+
mode=_compact_owner(mode),
|
|
550
|
+
owner=_compact_owner(owner),
|
|
551
|
+
group=_compact_owner(group),
|
|
552
|
+
modify=modify.strip(),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _value_after_label(line: str, label: str) -> str:
|
|
557
|
+
if label not in line:
|
|
558
|
+
return ""
|
|
559
|
+
return line.split(label, 1)[1].strip()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _field_between(line: str, start: str, end: str) -> str:
|
|
563
|
+
if start not in line or end not in line:
|
|
564
|
+
return ""
|
|
565
|
+
return line.split(start, 1)[1].split(end, 1)[0].strip()
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _paren_after(line: str, label: str) -> str:
|
|
569
|
+
start = line.find(label)
|
|
570
|
+
if start < 0:
|
|
571
|
+
return ""
|
|
572
|
+
open_index = line.find("(", start)
|
|
573
|
+
close_index = line.find(")", open_index + 1)
|
|
574
|
+
if open_index < 0 or close_index < 0:
|
|
575
|
+
return ""
|
|
576
|
+
return line[open_index + 1 : close_index].strip()
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _compact_owner(value: str | None) -> str:
|
|
580
|
+
if not value:
|
|
581
|
+
return ""
|
|
582
|
+
return "/".join(part.strip() for part in value.split("/"))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _looks_like_path(path: str) -> bool:
|
|
586
|
+
return (
|
|
587
|
+
path == "total"
|
|
588
|
+
or path.startswith(("/", "./", "../", "~"))
|
|
589
|
+
or "/" in path
|
|
590
|
+
or path in {".", ".."}
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _looks_like_file_path(path: str) -> bool:
|
|
595
|
+
name = path.rsplit("/", 1)[-1]
|
|
596
|
+
return _looks_like_path(path) or "." in name
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _unique_sorted(values: list[int]) -> list[int]:
|
|
600
|
+
return sorted(set(values))
|