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.
Files changed (72) hide show
  1. codetool_shell/__init__.py +11 -0
  2. codetool_shell/api.py +59 -0
  3. codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
  4. codetool_shell/filters/__init__.py +14 -0
  5. codetool_shell/filters/build_compiler/__init__.py +7 -0
  6. codetool_shell/filters/build_compiler/detector.py +412 -0
  7. codetool_shell/filters/build_compiler/reducer.py +166 -0
  8. codetool_shell/filters/build_compiler/summary.py +617 -0
  9. codetool_shell/filters/ci_job_log/__init__.py +7 -0
  10. codetool_shell/filters/ci_job_log/detector.py +64 -0
  11. codetool_shell/filters/ci_job_log/reducer.py +99 -0
  12. codetool_shell/filters/ci_job_log/summary.py +243 -0
  13. codetool_shell/filters/diff/__init__.py +7 -0
  14. codetool_shell/filters/diff/detector.py +136 -0
  15. codetool_shell/filters/diff/reducer.py +308 -0
  16. codetool_shell/filters/generic_log/__init__.py +7 -0
  17. codetool_shell/filters/generic_log/detector.py +175 -0
  18. codetool_shell/filters/generic_log/reducer.py +99 -0
  19. codetool_shell/filters/generic_log/summary.py +161 -0
  20. codetool_shell/filters/git.py +514 -0
  21. codetool_shell/filters/html_cleanup/__init__.py +7 -0
  22. codetool_shell/filters/html_cleanup/detector.py +136 -0
  23. codetool_shell/filters/html_cleanup/reducer.py +27 -0
  24. codetool_shell/filters/html_cleanup/summary.py +422 -0
  25. codetool_shell/filters/json_payload/__init__.py +7 -0
  26. codetool_shell/filters/json_payload/detector.py +62 -0
  27. codetool_shell/filters/json_payload/reducer.py +81 -0
  28. codetool_shell/filters/json_payload/summary.py +233 -0
  29. codetool_shell/filters/listing/__init__.py +7 -0
  30. codetool_shell/filters/listing/detector.py +294 -0
  31. codetool_shell/filters/listing/reducer.py +30 -0
  32. codetool_shell/filters/log_template/__init__.py +7 -0
  33. codetool_shell/filters/log_template/constants.py +76 -0
  34. codetool_shell/filters/log_template/detector.py +331 -0
  35. codetool_shell/filters/log_template/reducer.py +78 -0
  36. codetool_shell/filters/log_template/template.py +280 -0
  37. codetool_shell/filters/log_template/types.py +21 -0
  38. codetool_shell/filters/opaque_payload/__init__.py +7 -0
  39. codetool_shell/filters/opaque_payload/detector.py +563 -0
  40. codetool_shell/filters/opaque_payload/reducer.py +142 -0
  41. codetool_shell/filters/opaque_payload/summary.py +61 -0
  42. codetool_shell/filters/package_manager/__init__.py +7 -0
  43. codetool_shell/filters/package_manager/detector.py +220 -0
  44. codetool_shell/filters/package_manager/reducer.py +110 -0
  45. codetool_shell/filters/package_manager/summary.py +172 -0
  46. codetool_shell/filters/pipeline.py +65 -0
  47. codetool_shell/filters/rg.py +250 -0
  48. codetool_shell/filters/system_output/__init__.py +7 -0
  49. codetool_shell/filters/system_output/detector.py +600 -0
  50. codetool_shell/filters/system_output/reducer.py +331 -0
  51. codetool_shell/filters/system_output/summary.py +164 -0
  52. codetool_shell/filters/table/__init__.py +7 -0
  53. codetool_shell/filters/table/detector.py +244 -0
  54. codetool_shell/filters/table/reducer.py +57 -0
  55. codetool_shell/filters/table/summary.py +37 -0
  56. codetool_shell/filters/test_runner/__init__.py +7 -0
  57. codetool_shell/filters/test_runner/ansi.py +80 -0
  58. codetool_shell/filters/test_runner/detector.py +409 -0
  59. codetool_shell/filters/test_runner/reducer.py +288 -0
  60. codetool_shell/filters/test_runner/summary.py +449 -0
  61. codetool_shell/filters/text.py +38 -0
  62. codetool_shell/filters/traceback/__init__.py +7 -0
  63. codetool_shell/filters/traceback/detector.py +209 -0
  64. codetool_shell/filters/traceback/reducer.py +141 -0
  65. codetool_shell/filters/traceback/summary.py +122 -0
  66. codetool_shell/filters/tree.py +59 -0
  67. codetool_shell/py.typed +0 -0
  68. codetool_shell/python_backend.py +38 -0
  69. codetool_shell/rust_backend.py +254 -0
  70. codetool_shell-0.1.1.dist-info/METADATA +152 -0
  71. codetool_shell-0.1.1.dist-info/RECORD +72 -0
  72. 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))