sanitized rig
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Render per-room manifests for deploy into the shared `spr` kind cluster.
|
||||
|
||||
The `spr` cluster itself is created via `ctrl/kind-up.sh` at the repo root
|
||||
(one cluster, all rooms). Each room becomes a namespace inside it.
|
||||
The `spr` cluster itself is created by `make cluster up` at the repo root,
|
||||
which hands spr's shape to rig to build (one cluster, all rooms). Each room becomes a namespace inside it.
|
||||
|
||||
Called from build.py when a room opts in to k8s output. Emits:
|
||||
|
||||
|
||||
@@ -395,21 +395,21 @@ resources:
|
||||
|
||||
|
||||
# ─── Lifecycle scripts ──────────────────────────────────────────────
|
||||
# These target the shared `spr` kind cluster (created via repo-root
|
||||
# ctrl/kind-up.sh). Each room owns a namespace inside that cluster.
|
||||
# These target the shared `spr` kind cluster (created by `make cluster up`
|
||||
# at the repo root, which builds it with rig). Each room owns a namespace inside that cluster.
|
||||
|
||||
def k8s_up_sh(*, room: str, cluster: str, nodeport: int) -> str:
|
||||
return f"""\
|
||||
#!/bin/bash
|
||||
# Apply the "{room}" room into the shared `{cluster}` kind cluster.
|
||||
# (Run repo-root ctrl/kind-up.sh first if the cluster doesn't exist.)
|
||||
# (Run 'make cluster up' at the repo root first if the cluster doesn't exist.)
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
|
||||
K8S_DIR="$SCRIPT_DIR/k8s"
|
||||
|
||||
if ! kind get clusters 2>/dev/null | grep -q '^{cluster}$'; then
|
||||
echo "Kind cluster '{cluster}' not found — run ctrl/kind-up.sh from the repo root first."
|
||||
echo "Kind cluster '{cluster}' not found — run 'make cluster up' from the repo root first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -432,7 +432,7 @@ def k8s_down_sh(*, room: str, cluster: str) -> str:
|
||||
return f"""\
|
||||
#!/bin/bash
|
||||
# Remove the "{room}" namespace from the shared `{cluster}` cluster.
|
||||
# Leaves the cluster itself running (use repo-root ctrl/kind-down.sh to drop everything).
|
||||
# Leaves the cluster itself running (use 'make cluster down' at the repo root to drop everything).
|
||||
set -e
|
||||
|
||||
CTX="kind-{cluster}"
|
||||
|
||||
4
soleprint/station/tools/distill/.gitignore
vendored
Normal file
4
soleprint/station/tools/distill/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Local job: which repos this machine distills is not a fact about the tool, and
|
||||
# the list names whatever those repos are. Copy distill-example.json to
|
||||
# distill.json and edit that; it stays here.
|
||||
distill.json
|
||||
26
soleprint/station/tools/distill/distill-example.json
Normal file
26
soleprint/station/tools/distill/distill-example.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"_comment": "Template for distill.json, the job distill.sh reads when no repo is named on the command line. Copy this to distill.json (gitignored) and edit that. Everything that shapes the run is at the top; 'repos' is just a list of paths. Preview any change with: ./distill.sh list",
|
||||
|
||||
"_command": "tree = a directory per repo. digest = one .md per repo, flattened into a single readable file. both = each repo as a directory AND a .md. list = write nothing, just report what would be kept.",
|
||||
"command": "list",
|
||||
|
||||
"out": "distilled",
|
||||
|
||||
"_branch_mode": "full = each branch is a complete, standalone copy. diff = only the files that differ from diff_base (it gets a _PARTIAL.md saying so).",
|
||||
"branch_mode": "full",
|
||||
"diff_base": "main",
|
||||
|
||||
"_filters": "Applied to every repo. exclude/include are path globs; a pattern with no / also matches basenames at any depth. 'all' keeps the noise (lockfiles, images, minified, maps). max_bytes skips anything larger and names it in MANIFEST.md.",
|
||||
"exclude": [],
|
||||
"include": [],
|
||||
"all": false,
|
||||
"max_bytes": null,
|
||||
"skip_unchanged": true,
|
||||
"prune": true,
|
||||
|
||||
"_repos": "'path' is absolute, or a name resolved under --root. 'branches' is optional — leave it out for the working tree as it stands, uncommitted changes included. A path may appear more than once.",
|
||||
"repos": [
|
||||
{ "path": "/path/to/some-repo" },
|
||||
{ "path": "/path/to/another-repo", "branches": ["origin/main", "origin/feature/example"] }
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,75 @@
|
||||
# explode — one file back into the tree it describes
|
||||
|
||||
The other half of `distill.sh`. Together they are a round trip:
|
||||
|
||||
```
|
||||
distill.sh digest -> one file -> paste into a chat -> the reply -> explode.sh
|
||||
```
|
||||
|
||||
`distill` flattens repos into a single readable file so they fit somewhere that
|
||||
only takes text. `explode` takes the answer and writes it back onto disk. Neither
|
||||
is much use without the other.
|
||||
|
||||
```bash
|
||||
./explode.sh --list reply.md # what is in there; writes nothing
|
||||
./explode.sh -o ./restored reply.md # write the tree
|
||||
./explode.sh -o ./restored --force x.md # overwrite what is already there
|
||||
./explode.sh --contract > contract.txt # the format to hand to the model
|
||||
./explode.sh --selftest # check this copy against known input
|
||||
```
|
||||
|
||||
## Ask for `@@`, and attach it
|
||||
|
||||
`--contract` prints the output format to give whatever writes the reply. **Attach
|
||||
that file; do not paste it into the message.** A chat box renders markdown before
|
||||
the model sees it, and `===` alone under a line of text is setext syntax for a
|
||||
heading — so a pasted spec gets rendered as a title and the model is told nothing.
|
||||
`---` is worse, `##` is a heading, backticks open a fence. `@@` means nothing in
|
||||
markdown, which is exactly why it is the marker to ask for.
|
||||
|
||||
Keeping the wording in `--contract` rather than in a note somewhere means what you
|
||||
ask for cannot drift from what the parser accepts.
|
||||
|
||||
## Layouts
|
||||
|
||||
Four shapes are recognised, picked automatically; `--format` overrides the guess.
|
||||
|
||||
| shape | when |
|
||||
| --- | --- |
|
||||
| `@@ FILE: path` … `@@ END` | **ask for this** — explicit, and invisible to markdown |
|
||||
| `=== FILE: path` … `=== END` | the same thing, still read; do not ask for it |
|
||||
| `=== path` marker | a marker line, then the file until the next one |
|
||||
| `## path` + fenced block | `distill.sh`'s own digest |
|
||||
|
||||
Explicit open and close is worth insisting on: a writer emitting plain three-backtick
|
||||
fences silently truncates any file that itself contains a fence — every README with a
|
||||
shell example — because the nested fence looks exactly like the closing one.
|
||||
|
||||
## One reply, several projects
|
||||
|
||||
A thread usually touches more than one repo, and produces one file regardless. The
|
||||
contract asks for paths that begin with the project name, so point `-o` at the
|
||||
directory those projects sit in and each file lands in its own worktree:
|
||||
|
||||
```bash
|
||||
./ctrl/explode.sh --list bundle.txt # what is in there, write nothing
|
||||
./ctrl/explode.sh -o ./restored bundle.txt # write the tree
|
||||
./ctrl/explode.sh -o ./restored --force x.md # overwrite what is already there
|
||||
./explode.sh -o ~/wdir ~/Downloads/reply.md
|
||||
```
|
||||
|
||||
No mapping table to maintain, and a new project needs no change here.
|
||||
|
||||
## Refusals
|
||||
|
||||
Paths come out of a text file, so they are untrusted. Anything absolute or reaching
|
||||
upward with `..` is refused and **nothing** is written — the check runs over the whole
|
||||
input before the first file is created. An unclosed block is refused too, rather than
|
||||
writing the file short. Existing files are never overwritten without `--force`.
|
||||
|
||||
A digest whose files `distill` **clipped** to fit a token budget is refused for the
|
||||
same reason: the document holds only their head and tail, and a truncated file that
|
||||
reads complete is the failure the whole format exists to prevent. The tree copy beside
|
||||
the digest has them whole — take them from there.
|
||||
|
||||
Two limits worth knowing: a file whose last line had no trailing newline comes back
|
||||
with one, and in the bare `=== path` layout a line starting with `=== ` inside a file's
|
||||
own content cannot be told from a real marker. The digest and `@@` layouts have no such
|
||||
ambiguity.
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Explode one file back into the tree of files it describes.
|
||||
#
|
||||
# The inverse of ctrl/distill.sh's digest: something hands you a single text
|
||||
# The inverse of distill.sh's digest: something hands you a single text
|
||||
# file with many files inside it, each introduced by its path, and you want the
|
||||
# directory back.
|
||||
#
|
||||
# Three layouts are understood, picked automatically. Prefer the first if you
|
||||
# control what writes the file:
|
||||
#
|
||||
# === FILE: pkg/models/domain.py explicit open and close. Nothing has to be
|
||||
# @@ FILE: pkg/models/domain.py explicit open and close. Nothing has to be
|
||||
# <the file> counted or inferred, and a block that is
|
||||
# === END never closed is an error rather than a
|
||||
# @@ END never closed is an error rather than a
|
||||
# file quietly missing its tail.
|
||||
#
|
||||
# === FILE: pkg/models/domain.py the same thing with '===' instead of '@@'.
|
||||
# <the file> Still read, but do not ask for it: see the
|
||||
# === END note on markdown below.
|
||||
#
|
||||
# === ./pkg/models/domain.py a marker line, then the file, until the
|
||||
# <the file> next marker or the end
|
||||
#
|
||||
@@ -30,12 +34,15 @@
|
||||
# -n same as --list
|
||||
# --force overwrite files that already exist
|
||||
# --format F fenced | marker | digest | auto (default: auto)
|
||||
# --contract print the output format to hand to whatever generates the file
|
||||
# --selftest check this copy of the script against known input and exit
|
||||
#
|
||||
# Examples:
|
||||
# explode.sh --list bundle.txt
|
||||
# explode.sh -o ./restored bundle.txt
|
||||
# explode.sh -o ./restored --force repo.md
|
||||
# explode.sh --contract > /tmp/contract.txt # attach this, do not paste it
|
||||
# explode.sh -o ~/wdir ~/Downloads/reply.md # one reply, every project
|
||||
#
|
||||
# Why the explicit form is worth asking for: a writer that emits plain three-
|
||||
# backtick fences truncates any file that itself contains a fence — every README
|
||||
@@ -43,6 +50,24 @@
|
||||
# exactly like the closing one. distill.sh avoids that by making its fences
|
||||
# longer than anything inside the file, but nothing else will bother.
|
||||
#
|
||||
# Ask for '@@', not '==='. A chat box renders markdown before the model sees the
|
||||
# message, and '===' alone on a line directly under text is setext syntax for a
|
||||
# level-one heading — so the format spec you paste gets swallowed and rendered as
|
||||
# a title, and the model is told nothing. '---' is worse (heading AND horizontal
|
||||
# rule), '##' is a heading, backticks open a fence. '@@' has no meaning in
|
||||
# markdown at all, which is the whole reason to use it. Both are parsed here, so
|
||||
# nothing already written stops working.
|
||||
#
|
||||
# The other half of that: put the spec in an attached file rather than the
|
||||
# message body. Attachments are not rendered. '--contract' prints the exact text
|
||||
# to attach, so the wording cannot drift from the parser that reads the reply
|
||||
# back.
|
||||
#
|
||||
# One reply, several projects. A thread produces one file however many repos it
|
||||
# touched, so --contract asks for paths that start with the project name. Point
|
||||
# -o at the directory those projects are siblings in and each file lands in its
|
||||
# own worktree — no table to maintain, and a new worktree needs no change here.
|
||||
#
|
||||
# Two limits worth knowing. A file whose last line has no trailing newline comes
|
||||
# back with one: the digest has to put a newline before the closing fence, so the
|
||||
# distinction is not in the input to recover. And in marker layout a line
|
||||
@@ -66,6 +91,7 @@ FORCE=""
|
||||
FORMAT="auto"
|
||||
SRC=""
|
||||
SELFTEST=""
|
||||
CONTRACT=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -73,6 +99,7 @@ while [ $# -gt 0 ]; do
|
||||
--list|-n) LIST=1 ;;
|
||||
--force) FORCE=1 ;;
|
||||
--format) shift; FORMAT="${1:-}" ;;
|
||||
--contract) CONTRACT=1 ;;
|
||||
--selftest) SELFTEST=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "unknown option: $1" ;;
|
||||
@@ -81,6 +108,43 @@ while [ $# -gt 0 ]; do
|
||||
shift
|
||||
done
|
||||
|
||||
# ── the contract ───────────────────────────────────────────────────────────
|
||||
# One copy of the wording, printed rather than remembered, so what you ask for
|
||||
# and what this parses cannot drift apart. Attach it; do not paste it into the
|
||||
# message body, where markdown gets a say first.
|
||||
contract() {
|
||||
cat <<'CONTRACT'
|
||||
OUTPUT FORMAT
|
||||
|
||||
Return every file you changed or created in full, one after another, using
|
||||
exactly this shape and nothing else:
|
||||
|
||||
@@ FILE: <project>/relative/path/to/file.py
|
||||
<the complete contents of the file>
|
||||
@@ END
|
||||
|
||||
Rules:
|
||||
|
||||
- One @@ FILE: line per file, and a matching @@ END line after its last line.
|
||||
- Start every path with the project it belongs to, spelled exactly as the
|
||||
heading of the document it came from, then the path relative to that
|
||||
project's root. One reply covers every project we touched; the prefix is
|
||||
the only thing that says which file goes where, so it is never optional
|
||||
and never abbreviated.
|
||||
- No leading ./ or /.
|
||||
- Between @@ FILE: and @@ END, emit the file verbatim. Do not wrap it in
|
||||
markdown fences, do not add line numbers, do not elide anything as
|
||||
"unchanged" or "...". A partial file is worse than no file.
|
||||
- Anything you want to say to me goes outside the blocks, before the first
|
||||
@@ FILE: or after the last @@ END. Text between blocks is ignored.
|
||||
- Return whole files only. No diffs, no patches, no hunks.
|
||||
- If a file's own content happens to contain a line starting with @@, say so
|
||||
in your prose so I know to check that block by hand.
|
||||
CONTRACT
|
||||
}
|
||||
|
||||
if [ -n "$CONTRACT" ]; then contract; exit 0; fi
|
||||
|
||||
# ── self-test ──────────────────────────────────────────────────────────────
|
||||
# So a copy of this script on another machine can be checked without any real
|
||||
# input, and without asking whether it is the version that knows a given format.
|
||||
@@ -137,6 +201,21 @@ FIXTURE
|
||||
check "wrong parser: refused" "1" "$([ -e "$t/d" ] && echo 0 || echo 1)"
|
||||
|
||||
# The other two layouts still work.
|
||||
# The @@ markers, which are the ones to ask a chat model for.
|
||||
printf '@@ FILE: pkg/a.py\nx = 1\n@@ END\n@@ FILE: b.md\n# t\n\n```sh\nls\n```\n@@ END\n' > "$t/g.txt"
|
||||
"$0" -o "$t/g" "$t/g.txt" >/dev/null 2>&1 || true
|
||||
check "at-markers: file count" "2" "$(find "$t/g" -type f 2>/dev/null | wc -l)"
|
||||
check "at-markers: fenced body" "2" "$(grep -c '```' "$t/g/b.md" 2>/dev/null || echo 0)"
|
||||
printf '@@ FILE: a.py\nx = 1\n' > "$t/h.txt"
|
||||
"$0" -o "$t/h" "$t/h.txt" >/dev/null 2>&1 || true
|
||||
check "at-markers: unterminated" "1" "$([ -e "$t/h" ] && echo 0 || echo 1)"
|
||||
|
||||
# One reply, several projects: the prefix is just the first directory.
|
||||
printf '@@ FILE: projA/a.py\nx = 1\n@@ END\n@@ FILE: projB/deep/b.py\ny = 2\n@@ END\n' > "$t/i.txt"
|
||||
"$0" -o "$t/i" "$t/i.txt" >/dev/null 2>&1 || true
|
||||
check "multi-project: first" "x = 1" "$(cat "$t/i/projA/a.py" 2>/dev/null)"
|
||||
check "multi-project: nested" "y = 2" "$(cat "$t/i/projB/deep/b.py" 2>/dev/null)"
|
||||
|
||||
printf '=== ./x/y.py\nz = 1\n' > "$t/e.txt"
|
||||
"$0" -o "$t/e" "$t/e.txt" >/dev/null 2>&1 || true
|
||||
check "marker layout" "z = 1" "$(cat "$t/e/x/y.py" 2>/dev/null)"
|
||||
@@ -145,6 +224,19 @@ FIXTURE
|
||||
"$0" -o "$t/f" "$t/f.txt" >/dev/null 2>&1 || true
|
||||
check "digest layout" "z = 1" "$(cat "$t/f/x/y.py" 2>/dev/null)"
|
||||
|
||||
# What distill actually writes: a metadata line between the heading and the
|
||||
# fence, and prose sections whose heading is followed by no fence at all.
|
||||
printf '# d\n\n## Tree\n\nx/\n y.py\n\n## x/y.py\n\n_1 lines · 6 bytes_\n\n```python\nz = 1\n```\n' > "$t/j.txt"
|
||||
"$0" -o "$t/j" "$t/j.txt" >/dev/null 2>&1 || true
|
||||
check "digest: metadata line" "z = 1" "$(cat "$t/j/x/y.py" 2>/dev/null)"
|
||||
check "digest: prose skipped" "1" "$(find "$t/j" -type f 2>/dev/null | wc -l)"
|
||||
|
||||
# A clipped file is head and tail only. Writing it would truncate the real
|
||||
# one, so the whole run is refused.
|
||||
printf '# d\n\n## x/y.py\n\n_900 lines · 60000 bytes · CLIPPED — head and tail only_\n\n```python\nz = 1\n```\n' > "$t/k.txt"
|
||||
"$0" -o "$t/k" "$t/k.txt" >/dev/null 2>&1 || true
|
||||
check "digest: clipped refused" "1" "$([ -e "$t/k" ] && echo 0 || echo 1)"
|
||||
|
||||
echo
|
||||
if [ "$rc" -eq 0 ]; then echo "all checks passed — this copy is current"
|
||||
else echo "SOME CHECKS FAILED — this copy is out of date or broken" >&2
|
||||
@@ -163,9 +255,9 @@ case "$FORMAT" in fenced|marker|digest|auto) ;; *) die "--format must be fenced,
|
||||
# markdown will contain plenty of '=== ' inside its own fenced content, and a
|
||||
# marker file can quote a '## ' heading just as easily.
|
||||
if [ "$FORMAT" = auto ]; then
|
||||
n_fenced=$(grep -cE '^=== +FILE: +[^ ]' "$SRC" || true)
|
||||
n_fenced=$(grep -cE '^(===|@@) +FILE: +[^ ]' "$SRC" || true)
|
||||
n_marker=$(grep -cE '^=== +\.?/?[^ ]' "$SRC" || true)
|
||||
n_marker=$((n_marker - n_fenced - $(grep -cE '^=== +END[ \t]*$' "$SRC" || true)))
|
||||
n_marker=$((n_marker - n_fenced - $(grep -cE '^(===|@@) +END[ \t]*$' "$SRC" || true)))
|
||||
[ "$n_marker" -lt 0 ] && n_marker=0
|
||||
n_digest=$(grep -cE '^## +[^ ]' "$SRC" || true)
|
||||
if [ "$n_fenced" -gt 0 ]; then
|
||||
@@ -220,7 +312,7 @@ parse() {
|
||||
|
||||
# Explicit open/close. The whole point is that nothing is inferred:
|
||||
# content is content until the END line, whatever it looks like.
|
||||
fmt == "fenced" && path == "" && /^=== +FILE: +/ {
|
||||
fmt == "fenced" && path == "" && /^(===|@@) +FILE: +/ {
|
||||
p = substr($0, index($0, "FILE:") + 5)
|
||||
sub(/^[ \t]+/, "", p)
|
||||
p = clean(p)
|
||||
@@ -228,7 +320,7 @@ parse() {
|
||||
open_file(p)
|
||||
next
|
||||
}
|
||||
fmt == "fenced" && path != "" && /^=== +END[ \t]*$/ { flush(); next }
|
||||
fmt == "fenced" && path != "" && /^(===|@@) +END[ \t]*$/ { flush(); next }
|
||||
fmt == "fenced" && path == "" { next } # anything between blocks is prose
|
||||
|
||||
fmt == "marker" && /^=== +/ {
|
||||
@@ -256,6 +348,22 @@ parse() {
|
||||
}
|
||||
fmt == "digest" && expect == 1 {
|
||||
if ($0 ~ /^[ \t]*$/) next # blank line between the two
|
||||
# distill.sh puts an italic "N lines, B bytes" line under each heading,
|
||||
# so the fence is no longer the next thing after it. Step over that
|
||||
# line rather than reading it as prose — without this, every file in
|
||||
# a current digest is skipped and the whole document looks empty.
|
||||
if ($0 ~ /^_.*_[ \t]*$/) {
|
||||
# Unless it says the file was clipped. A clipped body is head
|
||||
# and tail with a marker in between; writing it out would
|
||||
# replace a real file with a truncated one that reads complete,
|
||||
# which is the exact failure the unterminated check exists for.
|
||||
if ($0 ~ /CLIPPED/) {
|
||||
print "CLIPPED\t" pending
|
||||
bad = 1; expect = 0; pending = ""
|
||||
next
|
||||
}
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^`{3,}/) { # a fence: this is a file
|
||||
match($0, /^`+/)
|
||||
fence = substr($0, 1, RLENGTH)
|
||||
@@ -302,7 +410,7 @@ fi
|
||||
# looks complete is the failure this format exists to prevent.
|
||||
wrongfmt="$(printf '%s\n' "$scan" | grep '^WRONGFMT' || true)"
|
||||
if [ -n "$wrongfmt" ]; then
|
||||
echo "$SELF: this file uses '=== FILE: path' / '=== END', but it was read as" >&2
|
||||
echo "$SELF: this file uses 'FILE: path' / 'END' markers, but it was read as" >&2
|
||||
echo "the plain marker format, which would create a directory called 'FILE: .'" >&2
|
||||
echo "and files called 'END'. Re-run with --format fenced, or update this script." >&2
|
||||
exit 1
|
||||
@@ -310,13 +418,25 @@ fi
|
||||
|
||||
unterminated="$(printf '%s\n' "$scan" | grep '^UNTERMINATED' || true)"
|
||||
if [ -n "$unterminated" ]; then
|
||||
echo "$SELF: refusing — this block was never closed with '=== END':" >&2
|
||||
echo "$SELF: refusing — this block was never closed with '@@ END' or '=== END':" >&2
|
||||
printf '%s\n' "$unterminated" | sed 's/^UNTERMINATED\t/ /' >&2
|
||||
echo "the file it describes would be silently truncated" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
listing="$(printf '%s\n' "$scan" | grep -vE '^(UNSAFE|UNTERMINATED|WRONGFMT)' || true)"
|
||||
# distill clips the largest files to fit a budget, and says so. The tree copy
|
||||
# beside the digest has them whole, so the fix is to take them from there — not
|
||||
# to write out the head and tail under the real name.
|
||||
clipped="$(printf '%s\n' "$scan" | grep '^CLIPPED' || true)"
|
||||
if [ -n "$clipped" ]; then
|
||||
echo "$SELF: refusing — distill clipped these, so the digest has only their" >&2
|
||||
echo "head and tail:" >&2
|
||||
printf '%s\n' "$clipped" | sed 's/^CLIPPED\t/ /' >&2
|
||||
echo "take them from the tree copy instead; writing these would truncate them" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
listing="$(printf '%s\n' "$scan" | grep -vE '^(UNSAFE|UNTERMINATED|WRONGFMT|CLIPPED)' || true)"
|
||||
[ -n "$listing" ] || die "no files found in $SRC (format: $FORMAT)"
|
||||
count=$(printf '%s\n' "$listing" | grep -c . )
|
||||
|
||||
|
||||
Reference in New Issue
Block a user