@complexthings/superpowers-agent 9.0.0 → 9.0.2
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@complexthings/superpowers-agent",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.2",
|
|
4
4
|
"description": "Superpowers agent CLI — skills system for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
|
-
"url": "https://github.com/complexthings/superpowers.git"
|
|
36
|
+
"url": "git+https://github.com/complexthings/superpowers.git"
|
|
37
37
|
},
|
|
38
38
|
"bugs": {
|
|
39
39
|
"url": "https://github.com/complexthings/superpowers/issues"
|
|
@@ -1,132 +1,111 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: leveraging-cli-tools
|
|
3
|
-
description: Use when performing code searches, JSON parsing, file
|
|
3
|
+
description: Use when performing code searches, JSON/YAML parsing, file finding, structural refactors, or data wrangling, or when a command floods context with verbose output like logs, CI, or test runs - ensures agents reach for high-performance CLI tools (rg, jq, fd, yq, ast-grep, gh, sd) over slower standard tools like grep/find/sed and use them with discipline, choosing the right output-reducing flags, composing pipelines so raw output never enters context, and using a bundled reducer for output that flags cannot shape, cutting token cost and latency 5-50x. Check availability and offer to install a tool when a relevant task arises.
|
|
4
|
+
compatibility: The bundled scripts/slim.py needs python3 (standard on macOS/Linux). The CLI tools install via the system package manager.
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Leveraging CLI Tools
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Purpose
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Reach for high-performance CLI tools over slower standard tools. The leverage is filtering and transforming with the right tool **before reading**, so tokens and time go to the answer, not the search. On a large tree `rg` is 10-50x faster than `grep` and returns far less noise; across a session that compounds into hours and tens of thousands of tokens saved.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Picking the faster tool is only half of it. The other half is using it with discipline — two levers that keep raw output out of your context: **ask for less** (the right flags and selectors), and **shrink what you can't shape** (a reducer for inherently verbose output). A fast tool fed a lazy command still floods your context.
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
+
## When a relevant task arises
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
1. Pick the right tool from the table below.
|
|
18
|
+
2. Confirm it's installed before relying on it — e.g. `command -v rg`.
|
|
19
|
+
3. If it's missing, name the tool and its payoff and **offer** to install it — don't install silently. Adapt the command to the user's package manager/OS (the table shows `brew`; substitute `apt install`, `dnf install`, `pacman -S`, `cargo install`, etc.). If they decline, fall back to the standard tool and move on.
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
command -v rg jq fd bat gh >/dev/null 2>&1 && echo "ready" || echo "missing"
|
|
20
|
-
```
|
|
21
|
+
Check only the tools the current task needs — no upfront session-wide scan.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
**If tools ready:** Use optimal workflows throughout session.
|
|
23
|
+
## Tools
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
| Rating | Tool | Replaces | Why | Install |
|
|
26
|
+
|:------:|------|----------|-----|---------|
|
|
27
|
+
| 10 | `rg` (ripgrep) | `grep`, `grep -r`, `ack` | Code/text search. Respects `.gitignore`, 10-50x faster than `grep` — the highest-leverage tool; filter before reading. | `brew install ripgrep` |
|
|
28
|
+
| 10 | `jq` | `grep`/`sed`/`awk` on JSON | JSON query/transform. Turns API responses and config into exactly the fields you need; the pipe target for JSON. | `brew install jq` |
|
|
29
|
+
| 9 | `fd` | `find` | File finding. Faster, saner syntax, parallel traversal, `.gitignore`-aware. | `brew install fd` |
|
|
30
|
+
| 8 | `yq` | `grep`/`sed`/`awk` on YAML | `jq` for YAML/TOML/XML. Reads CI files, `docker-compose`, k8s manifests, frontmatter. | `brew install yq` |
|
|
31
|
+
| 8 | `ast-grep` (`sg`) | `sed`/`grep` for refactors | Structural search/rewrite by AST, not regex. Safe codebase-wide refactors that `sed` would mangle. | `brew install ast-grep` |
|
|
32
|
+
| 8 | `gh` | `curl` + GitHub API + tokens | GitHub from the shell — PRs, issues, CI, API. No hand-rolled `curl` + token juggling. | `brew install gh` |
|
|
33
|
+
| 7 | `sd` | `sed -i`, `perl -pe` | Find/replace. Literal-string-safe, no regex-escaping footguns. | `brew install sd` |
|
|
34
|
+
| 6 | `dasel` | `jq`+`yq`+`xq` (mixed formats) | Query *and modify* JSON/YAML/TOML/XML/CSV through one selector. Use when format is mixed or unknown. | `brew install dasel` |
|
|
35
|
+
| 6 | `htmlq` | `grep`/`sed` on HTML | `jq` for HTML — CSS-selector extraction from fetched pages. | `brew install htmlq` |
|
|
36
|
+
| 6 | `miller` (`mlr`) | `awk`/`cut`/`join`/`sort` on CSV/TSV | `awk`/`cut`/`join`/`sort` for CSV/TSV/JSON with *named* fields — no brittle column counting. | `brew install miller` |
|
|
37
|
+
| 5 | `qsv` | `awk`/`cut`/`sort -u` on CSV, `csvkit` | High-perf CSV toolkit (maintained `xsv` successor). Stats, slice, join, dedup on big CSVs. | `brew install qsv` |
|
|
38
|
+
| 5 | `hyperfine` | `time`, `for`-loop timing | Statistical benchmarking with warmups. Real before/after numbers, not `time` guesses. | `brew install hyperfine` |
|
|
39
|
+
| 5 | `tokei` | `wc -l`, `find … \| wc`, `cloc` | Instant LOC/language breakdown. Orient in an unfamiliar repo before exploring. | `brew install tokei` |
|
|
40
|
+
| 5 | `fzf` (`-f`) | manual fuzzy filtering | Non-interactive `-f`/`--filter` mode: fuzzy-rank a candidate list piped from `fd`/`rg`. | `brew install fzf` |
|
|
41
|
+
| 4 | `watchexec` | `while`+`sleep`, `entr` | Run a command on file change. Useful in build/test loops; non-interactive unlike most watchers. | `brew install watchexec` |
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|------|-----|-----------|---------|
|
|
29
|
-
| Search code | **rg** | grep | 10-50x |
|
|
30
|
-
| Parse JSON | **jq** | awk/sed | 5-20x |
|
|
31
|
-
| Find files | **fd** | find | 5-10x |
|
|
32
|
-
| View code | **bat** | cat | Better UX |
|
|
33
|
-
| Transform code | **ast-grep** | sed | 3-10x |
|
|
34
|
-
| GitHub ops | **gh** | curl+API | 2-5x |
|
|
35
|
-
| Interactive select | **fzf** | manual | 10-100x |
|
|
43
|
+
## Use them with discipline
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Five habits separate a fast, low-noise result from a slow, context-flooding one. The token savings below are measured, not hypothetical.
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
rg "AuthError" --type typescript # 10-50x faster than grep, respects .gitignore
|
|
42
|
-
```
|
|
47
|
+
**1 — Pick the tool that fits the data, not the one in muscle memory.** JSON → `jq`, YAML/TOML → `yq`, structural refactor → `ast-grep`, GitHub → `gh`. The reflex to resist is `grep`/`sed`/`awk` on structured data — they treat structure as flat text, so you pay in escaping bugs and noise the format-aware tool never produces.
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
jq '.results[] | select(.status=="error") | .error.code' api.json | sort -u
|
|
47
|
-
```
|
|
49
|
+
**2 — Ask for less: use the flag or selector that returns the answer's _shape_.** The biggest single win is never pulling raw output into context. Match the request to the question:
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
| Question | Flag / selector | Returns |
|
|
52
|
+
|----------|-----------------|---------|
|
|
53
|
+
| Which files match? | `rg -l` | paths only, no lines |
|
|
54
|
+
| How many files? | `rg -l \| wc -l` | a file count, not a line count |
|
|
55
|
+
| How many hits? | `rg -c` (lines/file) / `rg --count-matches` (matches/file) | counts only |
|
|
56
|
+
| Just the matched bit? | `rg -o` | the substring, not the whole line |
|
|
57
|
+
| Enough to judge a hit? | `rg -A/-B/-C N`, `rg -m N` (cap per file) | bounded context |
|
|
58
|
+
| Only certain fields? | `jq -r '.a, .b'`, `yq`, `mlr --opprint cut -f` | projected values, not the whole doc |
|
|
59
|
+
| First N of a list? | pipe to `head`, or `fd --max-results N` | the slice you need |
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
bat src/auth.ts # Syntax highlighting, Git integration, line numbers
|
|
57
|
-
```
|
|
61
|
+
For the **projection tools** (`jq`, `yq`, `dasel`, `miller`, `htmlq`) the selector *is* the filter — `jq` turning a 2,000-record array into one summed number is a ~100% reduction on its own. Shape their output at the selector; never post-filter it (that's what habit 4 is *not* for).
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
**3 — Compose so only the answer comes back.** Do the counting, dedup, and projection *in the pipeline*, not by reading raw output and reasoning over it. One pass keeps every intermediate result out of context:
|
|
60
64
|
```bash
|
|
61
|
-
|
|
65
|
+
rg -l '"error"' logs/ --type json | xargs jq -r 'select(.level=="error") | .code' | sort -u
|
|
62
66
|
```
|
|
63
|
-
|
|
64
|
-
## Installation Protocol
|
|
65
|
-
|
|
66
|
-
**If tool missing when needed:**
|
|
67
|
-
|
|
68
|
-
1. Explain impact: "Using rg is 10-50x faster, reducing token costs"
|
|
69
|
-
2. Install automatically for core tools (rg, jq, fd, bat, gh): `brew install ripgrep jq fd bat gh` (macOS) or equivalent
|
|
70
|
-
3. Ask before installing: ast-grep, httpie, fzf
|
|
71
|
-
|
|
72
|
-
**No exceptions:** If tool unavailable and user declines install, explain performance cost but use fallback.
|
|
73
|
-
|
|
74
|
-
## Key Workflows
|
|
75
|
-
|
|
76
|
-
### Filter Before Reading
|
|
67
|
+
This is the lesson of programmatic tool calling (Amazon's PTC benchmarks: ~87-92% fewer tokens): when a task needs several tool calls plus processing, write **one** pipeline or script that does it all and returns only the result — the intermediate data never touches your context. So: filter before reading, and refactor by AST, not regex:
|
|
77
68
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
rg "password.*hash" src/auth/ --type ts -l | xargs bat
|
|
69
|
+
rg -l "password.*hash" src/auth/ --type ts | xargs rg "TODO"
|
|
70
|
+
sg --pattern 'console.log($$$A)' --rewrite 'logger.debug($$$A)' --lang ts
|
|
81
71
|
```
|
|
82
72
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
### Compose Tools
|
|
73
|
+
**4 — Shrink what you can't shape: the bundled reducer for verbose emitters.** Some output has no projection flag — logs, CI output (`gh run view --log`), test runs, stack traces, status dumps. Flags get you part way, but the residual stays repetitive, wide, and noisy. Pipe it through `scripts/slim.py` (relative to this skill; use its absolute path if you're working elsewhere) and ask only for the cuts the task needs:
|
|
86
74
|
```bash
|
|
87
|
-
|
|
75
|
+
gh run view --log | python3 scripts/slim.py --errors --uniq --max-line 200
|
|
76
|
+
cargo test 2>&1 | python3 scripts/slim.py --errors --dedup
|
|
77
|
+
rg -n TODO --no-ignore | python3 scripts/slim.py --group-dir
|
|
88
78
|
```
|
|
79
|
+
It does dedup (`--dedup`/`--uniq`, repeats collapse to `(xN)`), grouping (`--group-dir` → per-directory counts), truncation (`--head/--tail/--middle/--max-line`), and noise-stripping (`--comments/--errors/--grep`). On a real 1,236-line CI log, `rg` error-filtering alone cut 91% but left ~6,700 tokens; adding `slim` reached 97% — a third the residual. Run `python3 scripts/slim.py -h` for all modes. The cuts are **lossy and opt-in**: ask for what you need, know what you're discarding, and don't reach for it when a projection tool's own selector would do the job.
|
|
89
80
|
|
|
90
|
-
|
|
81
|
+
**5 — Read the output critically — it's a claim, not a fact.** A zero or suspiciously low result is the one to distrust: `rg` skips `.gitignore`d and hidden files by default, so a real match in `node_modules/`, `dist/`, or a dotfile is silently absent. Before concluding "none," re-run with `--no-ignore`, `-uu`, or `--hidden` — then decide whether those ignored hits (vendored deps, build output, generated mirrors) actually belong in the answer; surfacing them is the check, keeping them is a judgment call. Mind case (`-i`) and word boundaries (`-w`) so you neither miss `Subagent` nor over-match `tasks` — and when an identifier doubles as a common word (a `Task` tool vs. the word "task"), even `-w` isn't enough: glance at the context to confirm the hit is the symbol, not prose. When a count or list drives a decision, confirm a match means what you think before acting on it.
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|------|---------------|-----------|---------|
|
|
94
|
-
| Search 50k files | 45s | 0.8s | 56x, ~40k tokens |
|
|
95
|
-
| Parse 10MB JSON | 12s (awk) | 2s (jq) | 6x, ~15k tokens |
|
|
96
|
-
| Find in monorepo | 8s | 1s | 8x, ~10k tokens |
|
|
83
|
+
In Claude Code the `Grep` and `Glob` tools are themselves built on ripgrep — prefer them for in-context searches, and reach for the CLI tools when you need piping, transforms, rewrites, or output reduction.
|
|
97
84
|
|
|
98
|
-
## Red
|
|
85
|
+
## Red flags
|
|
99
86
|
|
|
100
|
-
-
|
|
101
|
-
- Reading files
|
|
102
|
-
-
|
|
103
|
-
-
|
|
87
|
+
- Parsing JSON/YAML with `awk`/`sed`/`grep` instead of `jq`/`yq`.
|
|
88
|
+
- Reading files before filtering them with `rg`.
|
|
89
|
+
- Piping a tool's full output into context to eyeball it, when `-l`/`-c`/`-o` or a `jq` projection would return just the answer.
|
|
90
|
+
- Letting a verbose command (test run, CI log, `git`/`docker` status dump) land raw in context when `slim` would cut it 90%+.
|
|
91
|
+
- Reaching for `slim` on a projection tool's output — shape it at the `jq`/`yq`/`mlr` selector instead.
|
|
92
|
+
- Concluding "no matches" from a default `rg` run without re-checking `--no-ignore`/`--hidden` — the hit may be sitting in an ignored directory.
|
|
93
|
+
- Hand-rolling `curl` against the GitHub API instead of `gh`.
|
|
94
|
+
- Regex codemods with `sed` where `ast-grep` is structurally safe.
|
|
104
95
|
|
|
105
|
-
## Common
|
|
96
|
+
## Common rationalizations
|
|
106
97
|
|
|
107
98
|
| Excuse | Reality |
|
|
108
99
|
|--------|---------|
|
|
109
|
-
| "grep works fine" | 50x slower
|
|
110
|
-
| "I don't know if they have jq" |
|
|
111
|
-
| "Not worth setup
|
|
112
|
-
| "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
**Use when:**
|
|
122
|
-
- Codebase search (always)
|
|
123
|
-
- JSON operations (always)
|
|
124
|
-
- Time pressure, large datasets, repeated operations
|
|
125
|
-
|
|
126
|
-
## Summary
|
|
127
|
-
|
|
128
|
-
**Protocol: Check → Use → Combine**
|
|
129
|
-
|
|
130
|
-
Session start: Silent availability check. Task appears: Use optimal tool or install. Complex tasks: Compose tools.
|
|
131
|
-
|
|
132
|
-
**Not using available tools burns resources.**
|
|
100
|
+
| "grep works fine" | On a big tree it's 10-50x slower and floods context with noise `rg` would have filtered out. |
|
|
101
|
+
| "I don't know if they have jq" | One `command -v jq` answers it; install is seconds and pays back across the whole session. |
|
|
102
|
+
| "Not worth the setup" | One install = a speedup on every future task, not just this one. |
|
|
103
|
+
| "The test/CI output is just long, I'll scroll it" | A 1,200-line log is ~75k tokens of mostly noise; `slim --errors --uniq` makes it ~2k without losing the failures. |
|
|
104
|
+
| "User didn't ask for optimization" | Faster, lower-noise completion *is* better completion. |
|
|
105
|
+
|
|
106
|
+
## When NOT to use
|
|
107
|
+
|
|
108
|
+
- A tiny one-off (a handful of files, well under a megabyte) where the standard tool is already at hand.
|
|
109
|
+
- The user declined the install — note the cost once, then use the fallback.
|
|
110
|
+
- A teaching context where the standard tool is the point.
|
|
111
|
+
- Output you already shaped with a selector — don't add a `slim` pass for its own sake.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""slim - reduce noisy command output to signal before it reaches your context.
|
|
3
|
+
|
|
4
|
+
A stdin->stdout filter for output you CAN'T shape with a tool's own flags:
|
|
5
|
+
logs, scan results, status dumps, stack traces, repeated lines. For tools that
|
|
6
|
+
already project output (jq/yq/dasel/miller/htmlq), use their selectors instead;
|
|
7
|
+
piping them through slim is redundant.
|
|
8
|
+
|
|
9
|
+
Pipe a verbose command through it and ask only for the reductions you need:
|
|
10
|
+
|
|
11
|
+
cargo test 2>&1 | slim --errors --dedup
|
|
12
|
+
rg -n TODO --no-ignore | slim --group-dir
|
|
13
|
+
gh run view --log | slim --uniq --max-line 200 --middle 40
|
|
14
|
+
kubectl get events | slim --dedup --stats
|
|
15
|
+
|
|
16
|
+
Reductions (compose freely):
|
|
17
|
+
--comments drop comment-only lines (# // ; -- *) and blank lines
|
|
18
|
+
--errors keep only lines matching error|warn|fail|fatal|panic|exception (-i)
|
|
19
|
+
--grep RE keep only lines matching regex RE (use --invert to drop them)
|
|
20
|
+
--invert invert --grep / --errors (drop matches instead of keeping)
|
|
21
|
+
--dedup collapse ALL repeated lines, first-seen order, annotate " (xN)"
|
|
22
|
+
--uniq collapse only CONSECUTIVE repeats, annotate " (xN)" (log-friendly)
|
|
23
|
+
--group-dir turn a list of file paths into per-directory counts
|
|
24
|
+
--max-line N truncate each line to N chars, append a horizontal ellipsis
|
|
25
|
+
--head N keep first N lines
|
|
26
|
+
--tail N keep last N lines
|
|
27
|
+
--middle N keep first N and last N lines, replace the gap with a marker
|
|
28
|
+
--stats print "lines A->B, bytes A->B (-P%)" to stderr (output unaffected)
|
|
29
|
+
|
|
30
|
+
Always-on, lossless: trailing whitespace is stripped and runs of blank lines
|
|
31
|
+
collapse to one. Everything lossy is opt-in. Order: filter -> transform ->
|
|
32
|
+
dedup -> group -> blank-collapse -> truncate. Reductions are LOSSY by design;
|
|
33
|
+
ask for what the task needs and know what you're discarding.
|
|
34
|
+
"""
|
|
35
|
+
import argparse
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
COMMENT_RE = re.compile(r"^\s*(#|//|;|--|\*|/\*|\*/)")
|
|
40
|
+
ERROR_RE = re.compile(r"error|warn|fail|fatal|panic|exception|traceback", re.IGNORECASE)
|
|
41
|
+
ELLIPSIS = "…"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def annotate(line: str, n: int) -> str:
|
|
45
|
+
return line if n == 1 else f"{line} (x{n})"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dedup_all(lines):
|
|
49
|
+
counts, order = {}, []
|
|
50
|
+
for ln in lines:
|
|
51
|
+
if ln not in counts:
|
|
52
|
+
counts[ln] = 0
|
|
53
|
+
order.append(ln)
|
|
54
|
+
counts[ln] += 1
|
|
55
|
+
return [annotate(ln, counts[ln]) for ln in order]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def dedup_adjacent(lines):
|
|
59
|
+
out, prev, n = [], None, 0
|
|
60
|
+
for ln in lines:
|
|
61
|
+
if ln == prev:
|
|
62
|
+
n += 1
|
|
63
|
+
else:
|
|
64
|
+
if prev is not None:
|
|
65
|
+
out.append(annotate(prev, n))
|
|
66
|
+
prev, n = ln, 1
|
|
67
|
+
if prev is not None:
|
|
68
|
+
out.append(annotate(prev, n))
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def group_dir(lines):
|
|
73
|
+
counts, order = {}, []
|
|
74
|
+
for ln in lines:
|
|
75
|
+
token = ln.split(":", 1)[0].strip() # tolerate rg's path:line: prefix
|
|
76
|
+
d = token.rsplit("/", 1)[0] + "/" if "/" in token else "./"
|
|
77
|
+
if d not in counts:
|
|
78
|
+
counts[d] = 0
|
|
79
|
+
order.append(d)
|
|
80
|
+
counts[d] += 1
|
|
81
|
+
width = max((len(d) for d in order), default=0)
|
|
82
|
+
return [f"{d.ljust(width)} ({counts[d]} file{'s' if counts[d] != 1 else ''})" for d in order]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def collapse_blanks(lines):
|
|
86
|
+
out, blank = [], False
|
|
87
|
+
for ln in lines:
|
|
88
|
+
if ln.strip() == "":
|
|
89
|
+
if not blank:
|
|
90
|
+
out.append("")
|
|
91
|
+
blank = True
|
|
92
|
+
else:
|
|
93
|
+
out.append(ln)
|
|
94
|
+
blank = False
|
|
95
|
+
while out and out[-1] == "":
|
|
96
|
+
out.pop()
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
p = argparse.ArgumentParser(add_help=True, description="Reduce noisy stdout to signal.")
|
|
102
|
+
p.add_argument("--comments", action="store_true")
|
|
103
|
+
p.add_argument("--errors", action="store_true")
|
|
104
|
+
p.add_argument("--grep", metavar="RE")
|
|
105
|
+
p.add_argument("--invert", action="store_true")
|
|
106
|
+
p.add_argument("--dedup", action="store_true")
|
|
107
|
+
p.add_argument("--uniq", action="store_true")
|
|
108
|
+
p.add_argument("--group-dir", action="store_true")
|
|
109
|
+
p.add_argument("--max-line", type=int, metavar="N")
|
|
110
|
+
p.add_argument("--head", type=int, metavar="N")
|
|
111
|
+
p.add_argument("--tail", type=int, metavar="N")
|
|
112
|
+
p.add_argument("--middle", type=int, metavar="N")
|
|
113
|
+
p.add_argument("--stats", action="store_true")
|
|
114
|
+
args = p.parse_args()
|
|
115
|
+
|
|
116
|
+
raw = sys.stdin.read()
|
|
117
|
+
in_lines = raw.splitlines()
|
|
118
|
+
in_bytes = len(raw.encode("utf-8", "replace"))
|
|
119
|
+
|
|
120
|
+
lines = [ln.rstrip() for ln in in_lines]
|
|
121
|
+
|
|
122
|
+
if args.comments:
|
|
123
|
+
lines = [ln for ln in lines if ln.strip() and not COMMENT_RE.match(ln)]
|
|
124
|
+
|
|
125
|
+
if args.errors or args.grep:
|
|
126
|
+
pat = re.compile(args.grep) if args.grep else ERROR_RE
|
|
127
|
+
keep = (lambda ln: not pat.search(ln)) if args.invert else (lambda ln: bool(pat.search(ln)))
|
|
128
|
+
lines = [ln for ln in lines if keep(ln)]
|
|
129
|
+
|
|
130
|
+
if args.max_line and args.max_line > 0:
|
|
131
|
+
lines = [ln if len(ln) <= args.max_line else ln[: args.max_line] + ELLIPSIS for ln in lines]
|
|
132
|
+
|
|
133
|
+
if args.dedup:
|
|
134
|
+
lines = dedup_all(lines)
|
|
135
|
+
elif args.uniq:
|
|
136
|
+
lines = dedup_adjacent(lines)
|
|
137
|
+
|
|
138
|
+
if args.group_dir:
|
|
139
|
+
lines = group_dir(lines)
|
|
140
|
+
|
|
141
|
+
lines = collapse_blanks(lines)
|
|
142
|
+
|
|
143
|
+
if args.middle and args.middle > 0 and len(lines) > 2 * args.middle:
|
|
144
|
+
omitted = len(lines) - 2 * args.middle
|
|
145
|
+
lines = lines[: args.middle] + [f"{ELLIPSIS} {omitted} lines omitted {ELLIPSIS}"] + lines[-args.middle :]
|
|
146
|
+
if args.head and args.head > 0:
|
|
147
|
+
lines = lines[: args.head]
|
|
148
|
+
if args.tail and args.tail > 0:
|
|
149
|
+
lines = lines[-args.tail :]
|
|
150
|
+
|
|
151
|
+
out = "\n".join(lines)
|
|
152
|
+
if out:
|
|
153
|
+
out += "\n"
|
|
154
|
+
sys.stdout.write(out)
|
|
155
|
+
|
|
156
|
+
if args.stats:
|
|
157
|
+
out_bytes = len(out.encode("utf-8", "replace"))
|
|
158
|
+
pct = 0 if in_bytes == 0 else round(100 * (1 - out_bytes / in_bytes))
|
|
159
|
+
change = f"-{pct}%" if pct >= 0 else f"+{-pct}% larger"
|
|
160
|
+
sys.stderr.write(
|
|
161
|
+
f"slim: lines {len(in_lines)}->{len(lines)}, bytes {in_bytes}->{out_bytes} ({change})\n"
|
|
162
|
+
)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
sys.exit(main())
|