updated contract
This commit is contained in:
@@ -69,6 +69,8 @@
|
||||
# foo the working tree, as it is now (uncommitted included)
|
||||
# foo@main one ref, read straight out of the object store
|
||||
# foo@main,topic several refs, each distilled separately
|
||||
# foo@3f2a9c1 a commit: a hash works anywhere a branch does,
|
||||
# and so does a tag or HEAD~3
|
||||
# foo@all every local branch
|
||||
# foo:src/api only that subtree
|
||||
# foo:src/api,docs several subtrees, as a single output
|
||||
|
||||
@@ -15,13 +15,46 @@ is much use without the other.
|
||||
./explode.sh -o ./restored reply.md # write the tree
|
||||
./explode.sh -o ./restored --force x.md # overwrite what is already there
|
||||
./explode.sh --raw -o ./restored x.md # leave ⟪BT3⟫ escapes as they are
|
||||
./explode.sh --contract > contract.txt # the format to hand to the model
|
||||
./explode.sh --contract > contract.txt # the instructions to hand to the model
|
||||
./explode.sh --check reply.md # test a reply to "check"; writes nothing
|
||||
./explode.sh --selftest # check this copy against known input
|
||||
```
|
||||
|
||||
## Test the format before the real work
|
||||
|
||||
A long reply that cannot be unpacked costs the whole wait. So test first, with a reply
|
||||
that takes seconds:
|
||||
|
||||
1. Start the conversation with `contract.txt` and the digest attached.
|
||||
2. Send the single word `check`. The contract tells the model to answer with a six-line
|
||||
file, `_check/check.md`, built to hit what breaks: a code fence that has to be written
|
||||
as `⟪BT3⟫`, a tab, straight quotes, the markers and the `~~~~~~~~` wrapper.
|
||||
3. Copy the reply with the **copy response** button, save it, and run
|
||||
`./explode.sh --check reply.md`.
|
||||
|
||||
It unpacks the reply through the real parser and compares the result byte for byte. On a
|
||||
pass, ask for the real work. On a failure it names each problem (escapes skipped, tab
|
||||
turned into spaces, curly quotes, missing wrapper, notes around the block) and prints a
|
||||
correction to paste back. A tab that arrives as spaces while the model wrote a tab means
|
||||
the copy is the problem, not the model.
|
||||
|
||||
The contract also sets how the model works, which is most of the waiting: no web search
|
||||
(everything is in the attachments), no preamble or summary, and batches.
|
||||
|
||||
A reply has an output length limit, and one that runs out mid-file loses that file. So
|
||||
for more than three files, or more than about 400 lines, the model first sends a plan
|
||||
with the files grouped into numbered batches (at most 5 files and about 400 lines each;
|
||||
a bigger file is a batch alone), waits for `go`, and then sends one batch per reply,
|
||||
headed `@@ BATCH n OF m` and ending `@@ MORE` until the last. `explode.sh` reads that
|
||||
line and says whether to answer `continue`. Explode each reply as it arrives; with the
|
||||
project prefix on every path they all land in the same tree. A reply that was cut off
|
||||
anyway is refused as unterminated, with a note to ask for smaller batches. When a real reply has literal backticks where
|
||||
`⟪BT3⟫` belonged, it still unpacks, and `explode.sh` lists those files so you can tell the
|
||||
model before the next one.
|
||||
|
||||
## Ask for `@@`, and attach it
|
||||
|
||||
`--contract` prints the output format to give whatever writes the reply. **Attach
|
||||
`--contract` prints the instructions to give whatever writes the reply: how to work, the output format, a worked example and the format check. **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.
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
# --force overwrite files that already exist
|
||||
# --raw leave ⟪BT3⟫-style escapes as they are (see below)
|
||||
# --format F fenced | marker | digest | auto (default: auto)
|
||||
# --contract print the output format to hand to whatever generates the file
|
||||
# --contract print the instructions to hand to whatever generates the file
|
||||
# --check F test a reply to the word "check" (see below), write nothing
|
||||
# --selftest check this copy of the script against known input and exit
|
||||
#
|
||||
# Examples:
|
||||
@@ -97,6 +98,14 @@
|
||||
# reply from the "copy response" button, not by selecting the rendered text —
|
||||
# the button gives the markdown as written.
|
||||
#
|
||||
# The format check. A long reply that turns out unusable costs the whole wait.
|
||||
# The contract defines a six-line check file that exercises what breaks: a code
|
||||
# fence that has to be escaped, a tab, straight quotes. Send the model the single
|
||||
# word "check", save its reply, and run explode.sh --check on it: it unpacks the
|
||||
# reply through the real parser, compares the result byte for byte, and says what
|
||||
# went wrong — escapes skipped, tabs turned into spaces by the copy, curly quotes,
|
||||
# missing wrapper — with a correction to paste back. Seconds, not a lost answer.
|
||||
#
|
||||
# Paths come out of a text file, so they are treated as untrusted: anything
|
||||
# absolute, or reaching upward with .., is refused and nothing is written. A
|
||||
# file that describes /etc/cron.d/x is not a file you want to expand blindly.
|
||||
@@ -114,6 +123,7 @@ FORMAT="auto"
|
||||
SRC=""
|
||||
SELFTEST=""
|
||||
CONTRACT=""
|
||||
CHECK=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -123,6 +133,7 @@ while [ $# -gt 0 ]; do
|
||||
--raw) RAW=1 ;;
|
||||
--format) shift; FORMAT="${1:-}" ;;
|
||||
--contract) CONTRACT=1 ;;
|
||||
--check) CHECK=1 ;;
|
||||
--selftest) SELFTEST=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "unknown option: $1" ;;
|
||||
@@ -137,10 +148,38 @@ done
|
||||
# message body, where markdown gets a say first.
|
||||
contract() {
|
||||
cat <<'CONTRACT'
|
||||
INSTRUCTIONS FOR THIS CONVERSATION
|
||||
|
||||
Read this whole file before replying, and follow it in every reply of this
|
||||
conversation. Your replies are unpacked into files by a script. A reply that
|
||||
does not follow the format below cannot be unpacked and is thrown away, however
|
||||
good the code in it is.
|
||||
|
||||
|
||||
HOW TO WORK
|
||||
|
||||
1. Everything you need is in the attached files. Do not search the web and do
|
||||
not cite sources. If something you need is missing, ask for it and stop.
|
||||
2. Do not restate the task, do not announce what you are about to do, and do
|
||||
not summarise what you did. Files first. Notes after, three sentences at most.
|
||||
3. If the request is ambiguous, ask one short question and stop. Do not write
|
||||
code for a guess.
|
||||
4. Replies have an output length limit, and a reply that runs out in the middle
|
||||
of a file loses that file. So when the change touches more than 3 files, or
|
||||
the files together are longer than about 400 lines, first reply with only a
|
||||
plan: the files grouped into numbered batches, one line per file saying what
|
||||
changes in it. A batch is at most 5 files and about 400 lines; a file longer
|
||||
than that is a batch on its own. Then wait for "go".
|
||||
5. Send one batch per reply, in order. Right after the opening ~~~~~~~~ line,
|
||||
write a line that is exactly @@ BATCH n OF m. If it is not the last batch,
|
||||
end the reply with a line that is exactly @@ MORE and wait for "continue".
|
||||
Never start a file you cannot finish in the same reply.
|
||||
|
||||
|
||||
OUTPUT FORMAT
|
||||
|
||||
Return every file you changed or created in full, one after another, using
|
||||
exactly this shape and nothing else:
|
||||
Return every file you changed or created in full, one after another, in exactly
|
||||
this shape:
|
||||
|
||||
~~~~~~~~
|
||||
@@ FILE: <project>/relative/path/to/file.py
|
||||
@@ -162,26 +201,137 @@ Rules:
|
||||
literal ⟪ as ⟪LQ⟫. Files you were given already use these escapes; copy
|
||||
them through verbatim. They are turned back into the real characters when
|
||||
the reply is unpacked; a real run of backticks breaks the reply.
|
||||
- Keep tabs as tabs and quotes as straight quotes (" and '). Never replace them
|
||||
with spaces or typographic quotes.
|
||||
- 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.
|
||||
project's root. 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 apart from those
|
||||
escapes. 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 fenced block, before or
|
||||
after it. Text between @@ blocks is ignored.
|
||||
escapes. Do not add line numbers, and do not elide anything as "unchanged"
|
||||
or "...". A partial file is worse than no file.
|
||||
- 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.
|
||||
- If a file's own content has a line starting with @@, say so in your notes.
|
||||
|
||||
|
||||
EXAMPLE
|
||||
|
||||
A complete reply returning one file whose content has a code block:
|
||||
|
||||
~~~~~~~~
|
||||
@@ FILE: myproject/docs/usage.md
|
||||
# Usage
|
||||
|
||||
⟪BT3⟫bash
|
||||
make run
|
||||
⟪BT3⟫
|
||||
@@ END FILE: myproject/docs/usage.md
|
||||
~~~~~~~~
|
||||
Added the usage page.
|
||||
|
||||
|
||||
FORMAT CHECK
|
||||
|
||||
When my whole message is the single word check, reply with nothing but one
|
||||
file in the output format above, and no notes. Its path is _check/check.md
|
||||
(no project prefix for this one), and its content is exactly these six lines:
|
||||
|
||||
line 1: # check
|
||||
line 2: three backticks immediately followed by the word bash
|
||||
line 3: echo "ok"
|
||||
line 4: three backticks
|
||||
line 5: one tab character, then the word indented
|
||||
line 6: done
|
||||
CONTRACT
|
||||
}
|
||||
|
||||
if [ -n "$CONTRACT" ]; then contract; exit 0; fi
|
||||
|
||||
# ── the format check ───────────────────────────────────────────────────────
|
||||
# What the contract's FORMAT CHECK asks for, as bytes once unpacked. Every line
|
||||
# is there to catch one way replies break: line 2 and 4 need the escape, line 3
|
||||
# straight quotes, line 5 a real tab.
|
||||
CHECK_WANT='# check\n```bash\necho "ok"\n```\n\tindented\ndone\n'
|
||||
|
||||
check_reply() {
|
||||
local src="$1" t problems="" warnings="" advice="" n
|
||||
t="$(mktemp -d)"; trap 'rm -rf "$t"' RETURN
|
||||
printf "$CHECK_WANT" > "$t/want"
|
||||
tr -d '\r' < "$src" > "$t/reply"
|
||||
|
||||
fail() { problems="$problems FAIL $1"$'\n'; advice="$advice- $2"$'\n'; }
|
||||
warn() { warnings="$warnings warn $1"$'\n'; advice="$advice- $2"$'\n'; }
|
||||
|
||||
if ! grep -qE '^@@ +FILE: +_check/check\.md[ \t]*$' "$t/reply"; then
|
||||
fail "no '@@ FILE: _check/check.md' line" \
|
||||
"Reply to check with the file _check/check.md in the output format, and nothing else."
|
||||
else
|
||||
awk '/^@@ +FILE: +_check\/check\.md[ \t]*$/ { on = 1; next }
|
||||
on && /^@@ +END/ { exit }
|
||||
on { print }' "$t/reply" > "$t/raw"
|
||||
if ! grep -qE '^@@ +END FILE: +_check/check\.md[ \t]*$' "$t/reply"; then
|
||||
fail "the block is not closed with '@@ END FILE: _check/check.md'" \
|
||||
"Close every file with an @@ END FILE: line repeating its path."
|
||||
fi
|
||||
if grep -q '```' "$t/raw"; then
|
||||
fail "real backticks inside the file" \
|
||||
"Inside file contents, write three backticks as ⟪BT3⟫, never as the characters."
|
||||
elif ! grep -q '⟪BT3⟫' "$t/raw"; then
|
||||
fail "no ⟪BT3⟫ where the code fence goes" \
|
||||
"Line 2 of the check is ⟪BT3⟫bash and line 4 is ⟪BT3⟫."
|
||||
fi
|
||||
if grep -q '[“”‘’]' "$t/raw"; then
|
||||
fail "typographic quotes instead of straight ones" \
|
||||
"Use straight quotes (\" and ') in file contents."
|
||||
fi
|
||||
if grep -qE '^ +indented' "$t/raw"; then
|
||||
fail "the tab on line 5 arrived as spaces" \
|
||||
"Keep tab characters as tabs. (If the model did write a tab, the copy converted it: use the copy-response button.)"
|
||||
fi
|
||||
if "$0" -o "$t/out" "$t/reply" > "$t/log" 2>&1 && [ -f "$t/out/_check/check.md" ]; then
|
||||
if ! cmp -s "$t/want" "$t/out/_check/check.md"; then
|
||||
fail "unpacked, but not the six expected lines:" \
|
||||
"The check file is exactly six lines: # check / ⟪BT3⟫bash / echo \"ok\" / ⟪BT3⟫ / a tab then indented / done."
|
||||
problems="$problems$(diff <(sed -n l "$t/want") <(sed -n l "$t/out/_check/check.md") | sed 's/^/ /' || true)"$'\n'
|
||||
fi
|
||||
else
|
||||
fail "explode could not unpack it: $(grep -v '^format:' "$t/log" | head -1)" \
|
||||
"Follow the output format exactly: markers on their own lines, nothing between the blocks."
|
||||
fi
|
||||
fi
|
||||
|
||||
n=$(grep -c '^~~~~~~~[ \t]*$' "$t/reply" || true)
|
||||
if [ "$n" -lt 2 ]; then
|
||||
warn "no ~~~~~~~~ lines around the block (unpacks, but the chat will render it badly)" \
|
||||
"Put all the blocks between two lines of eight tildes, and use no other fence."
|
||||
fi
|
||||
n=$(awk '/^~~~~~~~[ \t]*$/ { inside = !inside; next }
|
||||
!inside && /^@@ +FILE:/ { block = 1 }
|
||||
!inside && !block && NF { c++ }
|
||||
!inside && /^@@ +END/ { block = 0 }
|
||||
END { print c + 0 }' "$t/reply")
|
||||
if [ "$n" -gt 0 ]; then
|
||||
warn "$n line(s) of notes outside the block" \
|
||||
"For check, reply with the file only: no notes before or after."
|
||||
fi
|
||||
|
||||
if [ -z "$problems" ]; then
|
||||
echo " ok escapes, tab, quotes and markers all came through"
|
||||
[ -n "$warnings" ] && printf '%s' "$warnings"
|
||||
echo
|
||||
echo "format check passed"
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "$problems" "$warnings"
|
||||
echo
|
||||
echo "format check FAILED. Send this back to the model:"
|
||||
echo
|
||||
echo "The format check failed. Re-read the instructions file, fix these, and reply to check again:"
|
||||
printf '%s' "$advice"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 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.
|
||||
@@ -305,6 +455,35 @@ FIXTURE
|
||||
"$0" -o "$t/p" "$t/p.txt" >/dev/null 2>&1 || true
|
||||
check "digest: not escaped" '⟪BT3⟫' "$(cat "$t/p/x.md" 2>/dev/null)"
|
||||
|
||||
# The format check: a good reply passes, and each way replies break is named.
|
||||
good='~~~~~~~~\n@@ FILE: _check/check.md\n# check\n⟪BT3⟫bash\necho "ok"\n⟪BT3⟫\n\tindented\ndone\n@@ END FILE: _check/check.md\n~~~~~~~~\n'
|
||||
printf "$good" > "$t/q1.md"
|
||||
check "check: good reply" "0" "$("$0" --check "$t/q1.md" >/dev/null 2>&1; echo $?)"
|
||||
printf "$good" | sed 's/⟪BT3⟫/```/' > "$t/q2.md"
|
||||
check "check: real backticks" "1" "$("$0" --check "$t/q2.md" 2>&1 | grep -c 'real backticks')"
|
||||
printf "$good" | sed 's/^\tindented/ indented/' > "$t/q3.md"
|
||||
check "check: tab became spaces" "1" "$("$0" --check "$t/q3.md" 2>&1 | grep -c 'arrived as spaces')"
|
||||
printf "$good" | sed 's/"ok"/“ok”/' > "$t/q4.md"
|
||||
check "check: curly quotes" "1" "$("$0" --check "$t/q4.md" 2>&1 | grep -c 'typographic quotes')"
|
||||
{ echo "Sure! Here is the check."; printf "$good"; } > "$t/q5.md"
|
||||
check "check: notes only warn" "0" "$("$0" --check "$t/q5.md" >/dev/null 2>&1; echo $?)"
|
||||
printf "$good" | sed '/^done$/d' > "$t/q6.md"
|
||||
check "check: wrong content" "1" "$("$0" --check "$t/q6.md" 2>&1 | grep -c 'not the six expected')"
|
||||
|
||||
# Real replies: raw fences unpack but are reported, and @@ MORE is noticed.
|
||||
printf '@@ FILE: p/r.md\n# r\n```sh\nls\n```\ndone\n@@ END FILE: p/r.md\n@@ MORE\n' > "$t/r.txt"
|
||||
"$0" -o "$t/r" "$t/r.txt" > "$t/r.log" 2>&1 || true
|
||||
check "notes: raw fence reported" "1" "$(grep -c 'real ``` instead' "$t/r.log")"
|
||||
check "notes: @@ MORE noticed" "1" "$(grep -c '@@ MORE' "$t/r.log")"
|
||||
check "notes: still unpacked" "2" "$(grep -c '```' "$t/r/p/r.md" 2>/dev/null || echo 0)"
|
||||
printf '~~~~~~~~\n@@ BATCH 2 OF 3\n@@ FILE: p/b.py\nx = 1\n@@ END FILE: p/b.py\n~~~~~~~~\n@@ MORE\n' > "$t/s.txt"
|
||||
"$0" -o "$t/s" "$t/s.txt" > "$t/s.log" 2>&1 || true
|
||||
check "notes: batch n of m" "1" "$(grep -c 'batch 2 of 3: say "continue" for batch 3' "$t/s.log")"
|
||||
check "batch line is not a file" "x = 1" "$(cat "$t/s/p/b.py" 2>/dev/null)"
|
||||
printf '~~~~~~~~\n@@ BATCH 1 OF 2\n@@ FILE: p/c.py\nx = 1\n' > "$t/u.txt"
|
||||
"$0" -o "$t/u" "$t/u.txt" > "$t/u.log" 2>&1 || true
|
||||
check "cut-off reply explained" "1" "$(grep -c 'length limit' "$t/u.log")"
|
||||
|
||||
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
|
||||
@@ -314,6 +493,12 @@ FIXTURE
|
||||
|
||||
if [ -n "$SELFTEST" ]; then selftest; exit $?; fi
|
||||
|
||||
if [ -n "$CHECK" ]; then
|
||||
[ -n "$SRC" ] || die "--check needs the saved reply: $SELF --check reply.md"
|
||||
[ -f "$SRC" ] || die "no such file: $SRC"
|
||||
check_reply "$SRC"; exit $?
|
||||
fi
|
||||
|
||||
[ -n "$SRC" ] || { usage >&2; exit 1; }
|
||||
[ -f "$SRC" ] || die "no such file: $SRC"
|
||||
case "$FORMAT" in fenced|marker|digest|auto) ;; *) die "--format must be fenced, marker, digest or auto" ;; esac
|
||||
@@ -387,7 +572,14 @@ parse() {
|
||||
first = 2; last--
|
||||
} else last = n
|
||||
}
|
||||
if (mode == "list") printf "%s\t%d\n", path, last - first + 1
|
||||
if (mode == "list") {
|
||||
printf "%s\t%d\n", path, last - first + 1
|
||||
# Unpacks fine, but it is what breaks the chat rendering, and
|
||||
# what the model has to be told to stop doing.
|
||||
if (fmt == "fenced")
|
||||
for (i = first; i <= last; i++)
|
||||
if (buf[i] ~ /```/) { print "RAWFENCE\t" path; break }
|
||||
}
|
||||
else {
|
||||
out = dest "/" path
|
||||
d = out; sub(/\/[^\/]*$/, "", d)
|
||||
@@ -540,6 +732,7 @@ if [ -n "$mismatch" ]; then
|
||||
echo "$SELF: refusing — these blocks were closed with another file's name:" >&2
|
||||
printf '%s\n' "$mismatch" | awk -F'\t' '{ printf " opened %s, closed %s\n", $2, $3 }' >&2
|
||||
echo "a close went missing, so one block holds more than one file" >&2
|
||||
echo "tell the model: close every file with an @@ END FILE: line repeating its path, then resend those files" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -548,6 +741,8 @@ if [ -n "$unterminated" ]; then
|
||||
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
|
||||
echo "the reply most likely hit its length limit mid-file" >&2
|
||||
echo "tell the model: that file was cut off; resend it whole, closed with @@ END FILE: and its path, and keep batches smaller" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -563,13 +758,38 @@ if [ -n "$clipped" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
listing="$(printf '%s\n' "$scan" | grep -vE '^(UNSAFE|UNTERMINATED|WRONGFMT|CLIPPED|MISMATCH)' || true)"
|
||||
listing="$(printf '%s\n' "$scan" | grep -vE '^(UNSAFE|UNTERMINATED|WRONGFMT|CLIPPED|MISMATCH|RAWFENCE)' || true)"
|
||||
|
||||
# Not refusals: the reply unpacks. Said anyway, because each is something to
|
||||
# tell the model before the next reply rather than discover after it.
|
||||
notes() {
|
||||
local raw
|
||||
raw="$(printf '%s\n' "$scan" | grep '^RAWFENCE' | cut -f2 || true)"
|
||||
if [ -n "$raw" ]; then
|
||||
echo "note: these came back with real \`\`\` instead of ⟪BT3⟫ (unpacked fine; tell the model):" >&2
|
||||
printf '%s\n' "$raw" | sed 's/^/ /' >&2
|
||||
fi
|
||||
# The last batch line wins: a reply quoting an earlier one is still this batch.
|
||||
local batch n m
|
||||
batch="$(grep -oE '^@@ +BATCH +[0-9]+ +OF +[0-9]+' "$SRC" | tail -1 || true)"
|
||||
if [ -n "$batch" ]; then
|
||||
n="$(printf '%s' "$batch" | awk '{ print $3 }')"; m="$(printf '%s' "$batch" | awk '{ print $5 }')"
|
||||
if [ "$n" -lt "$m" ]; then
|
||||
echo "note: batch $n of $m: say \"continue\" for batch $((n + 1)), and explode that reply too" >&2
|
||||
else
|
||||
echo "note: batch $n of $m, the last one" >&2
|
||||
fi
|
||||
elif grep -qE '^@@ +MORE[ \t\r]*$' "$SRC"; then
|
||||
echo "note: the reply ends with @@ MORE: say \"continue\" and explode the next reply too" >&2
|
||||
fi
|
||||
}
|
||||
[ -n "$listing" ] || die "no files found in $SRC (format: $FORMAT)"
|
||||
count=$(printf '%s\n' "$listing" | grep -c . )
|
||||
|
||||
if [ -n "$LIST" ]; then
|
||||
printf '%s\n' "$listing" | awk -F'\t' '{ printf " %-60s %5d lines\n", $1, $2 }'
|
||||
echo "$count files"
|
||||
notes
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -591,3 +811,4 @@ mkdir -p "$DEST"
|
||||
parse write "$UNESC" >/dev/null
|
||||
printf '%s\n' "$listing" | awk -F'\t' '{ printf " %s\n", $1 }'
|
||||
echo "wrote $count files to $DEST"
|
||||
notes
|
||||
|
||||
Reference in New Issue
Block a user