Two efficiency findings from a code-review pass: - The separated (--config X) and joined (--config=X) argument branches duplicated ~20 lines of absolutize-if-relative path logic. Extracted into abs_config_value(), used by both branches; the redundant --config=/* special case falls out since the helper already passes absolute paths through unchanged. - The common single-file path spawned python3 twice per file (once for abspath resolution, once for flatten()). flatten() now optionally takes a tmpdir arg and does both in one process. The per-file loop under a directory argument is unchanged — that path wasn't flagged. agent-audit's copy is canonical; skill-audit's copy was regenerated via scripts/sync-vale-styles.sh, not hand-edited, to guarantee byte parity. No hardening (bash 3.2 compat, surrogateescape, symlink guards) touched. Verified: tests/test-vale-wrap.sh 39/39, check-vale-style-sync.sh clean, full suite 12/12.
527 lines
22 KiB
Bash
Executable File
527 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
|
|
# silently stops matching once the `description:` value spans 2+ physical lines
|
|
# in any form YAML joins back into one string — a `>`/`>-`/`>+` folded block
|
|
# scalar (the style used by most skills/agents in this repo), a plain scalar
|
|
# wrapped onto continuation lines, or a double- or single-quoted scalar wrapped
|
|
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
|
# value keeps exactly the line breaks the source has, and vale matches it fine
|
|
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
|
# This script flattens an affected description to a one-line scalar in a scratch
|
|
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
|
|
# `|-` literal block with a single content line, which vale matches just as well
|
|
# (padding with blank lines so every other line number is unchanged), then
|
|
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
|
# `vale` directly: same args, same exit code, bar the two documented divergences
|
|
# below.
|
|
#
|
|
# "Same args" means relative paths — path arguments and the values of the
|
|
# path-valued flags (`--config`, `--output`, `--path`) alike — resolve against
|
|
# the caller's current directory, exactly as bare `vale` resolves them. The flag
|
|
# values are rewritten to absolute form because the run ends up `cd`'d into the
|
|
# scratch mirror, where a relative one would no longer resolve. (An earlier
|
|
# version resolved path arguments against the repo root, an invented convention
|
|
# that hard-errored on `--config ../../.vale.ini` from a subdirectory and, worse,
|
|
# silently dropped file arguments that didn't happen to resolve from the repo
|
|
# root — skipping the flattening this script exists for.)
|
|
#
|
|
# Divergence 1: with no `--config` at all, this script's own sibling
|
|
# `assets/vale/.vale.ini` is used instead of vale's upward search. pre-commit
|
|
# prefixes only `entry[0]` with the hook-repo clone path, so a `--config` in
|
|
# `.pre-commit-hooks.yaml` would resolve against the *consuming* repo and
|
|
# hard-fail (E100) for every external consumer. The manifest therefore passes the
|
|
# script alone, and an explicit `--config` from any other caller still wins.
|
|
#
|
|
# Divergence 2: a path-shaped argument that does not exist is a hard error
|
|
# (exit 2). Bare vale drops it, falls back to reading stdin, and prints
|
|
# `0 errors ... in stdin` with exit 0 — a typo'd target is then indistinguishable
|
|
# from a clean run. Both audit skills treat a `0 files` report as NOT RUN rather
|
|
# than clean, and `in stdin` does not match that guard, so the silent form would
|
|
# read as "prefilter clean" and skip the LLM fallback. Erroring is the only way
|
|
# to keep that guard honest. Linting prose piped on stdin is therefore
|
|
# unsupported here — it already was, since the no-path handoff closes stdin so
|
|
# vale can't block on a pipe that will never carry content.
|
|
#
|
|
# Vale prints each path exactly as it was handed to it, so the scratch tree
|
|
# mirrors the caller's absolute cwd: a relative path argument is passed through
|
|
# verbatim and resolves to its flattened copy, keeping the report byte-identical
|
|
# to bare `vale`'s. An absolute path inside the cwd is relativized to keep that
|
|
# property. Only an absolute path outside the cwd is rewritten to its scratch
|
|
# copy and so reports a scratch path — unavoidable, since a file can only be
|
|
# read from where it actually is.
|
|
|
|
cwd="$(pwd -P)"
|
|
|
|
# Every array below is expanded as `${arr[@]+"${arr[@]}"}`: bash before 4.4 —
|
|
# including the 3.2 that macOS still ships as /bin/bash — treats `"${arr[@]}"`
|
|
# on an empty array as an unbound variable under `set -u`. No expansion site is
|
|
# reachable while empty on today's control flow, so this is insurance against a
|
|
# later edit breaking that invariant, not a live fix.
|
|
vale_args=()
|
|
path_args=()
|
|
pending_flag=""
|
|
config_given=false
|
|
|
|
# `--output` takes either one of vale's built-in style names or a template file
|
|
# path. Only the file form needs absolutizing, and the built-in names have to be
|
|
# excluded by name *before* the existence test below: a file or directory
|
|
# literally called `line` in the caller's cwd would otherwise rewrite the
|
|
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
|
|
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
|
|
# names — it is always a path — so the check is keyed on the flag too.
|
|
is_builtin_output() {
|
|
case "$2" in
|
|
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
|
|
*) false ;;
|
|
esac
|
|
}
|
|
# Absolutizes a `--config` value against the caller's cwd. Shared by both
|
|
# argument forms below — separated (`--config X`) and joined (`--config=X`)
|
|
# — so the "already absolute vs. needs $cwd prefixed" check lives in exactly
|
|
# one place instead of being duplicated per form.
|
|
abs_config_value() {
|
|
if [[ "$1" == /* ]]; then
|
|
printf '%s' "$1"
|
|
else
|
|
printf '%s' "$cwd/$1"
|
|
fi
|
|
}
|
|
for arg in "$@"; do
|
|
if [[ -n "$pending_flag" ]]; then
|
|
# Value of a separated two-argv flag. It is never a lint target, however
|
|
# file-like it looks. The run ends up `cd`'d into the scratch mirror, so a
|
|
# value naming a file has to be absolutized here or it stops resolving.
|
|
case "$pending_flag" in
|
|
--config)
|
|
# Always a path, and required to exist.
|
|
vale_args+=("$(abs_config_value "$arg")")
|
|
;;
|
|
--output|--path)
|
|
# See `is_builtin_output` above for why the built-in `--output` names
|
|
# are excluded first. Anything that names nothing is passed through and
|
|
# left for vale to interpret.
|
|
if is_builtin_output "$pending_flag" "$arg"; then
|
|
vale_args+=("$arg")
|
|
elif [[ "$arg" != /* && -e "$arg" ]]; then
|
|
vale_args+=("$cwd/$arg")
|
|
else
|
|
vale_args+=("$arg")
|
|
fi
|
|
;;
|
|
*)
|
|
vale_args+=("$arg")
|
|
;;
|
|
esac
|
|
pending_flag=""
|
|
continue
|
|
fi
|
|
case "$arg" in
|
|
--config)
|
|
vale_args+=("$arg")
|
|
pending_flag="$arg"
|
|
config_given=true
|
|
continue
|
|
;;
|
|
--config=*)
|
|
vale_args+=("--config=$(abs_config_value "${arg#--config=}")")
|
|
config_given=true
|
|
continue
|
|
;;
|
|
# Same cwd-relative resolution for the `--flag=value` spelling of the two
|
|
# other path-valued flags.
|
|
--output=*|--path=*)
|
|
flag_val="${arg#*=}"
|
|
if is_builtin_output "${arg%%=*}" "$flag_val"; then
|
|
vale_args+=("$arg")
|
|
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
|
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
|
else
|
|
vale_args+=("$arg")
|
|
fi
|
|
continue
|
|
;;
|
|
# Vale's remaining value-taking flags, per `vale --help` (3.x). In the
|
|
# separated two-argv form the value must not be classified as a lint target
|
|
# — `--output tmpl.tmpl` names a real template file, and treating it as
|
|
# input both lints the template and reorders argv so vale sees
|
|
# `--output --no-wrap`. The `--flag=value` form needs no entry here: it
|
|
# starts with `-` and falls through to vale untouched. A value flag added by
|
|
# some future vale release is simply absent from this list and lands back on
|
|
# today's behaviour, so this list going stale is never worse than not having
|
|
# it.
|
|
--ext|--filter|--glob|--minAlertLevel|--output|--path)
|
|
vale_args+=("$arg")
|
|
pending_flag="$arg"
|
|
continue
|
|
;;
|
|
# Vale's subcommands are bare words that name no file, so they would trip
|
|
# the not-found error below. A lint target literally named `sync` (no
|
|
# extension, no slash) is misread as the subcommand — accepted, because the
|
|
# alternative is failing every `vale-wrap.sh ls-config`.
|
|
ls-config|ls-dirs|ls-metrics|ls-vars|sync)
|
|
vale_args+=("$arg")
|
|
continue
|
|
;;
|
|
esac
|
|
if [[ "$arg" == -* ]]; then
|
|
vale_args+=("$arg")
|
|
continue
|
|
fi
|
|
# Everything left is a lint target: `vale [options] [input...]` has no third
|
|
# kind of argument. See divergence 2 above for why a missing one is fatal here.
|
|
if [[ ! -e "$arg" ]]; then
|
|
echo "vale-wrap.sh: no such file or directory: $arg" >&2
|
|
exit 2
|
|
fi
|
|
# An absolute path inside the caller's cwd is relativized so the report cites
|
|
# a path that resolves against the real tree. Left absolute, it would be
|
|
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
|
|
# path to a file that is deleted on exit, which reads as a bug in any report
|
|
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
|
# the scratch-path behaviour documented above.
|
|
if [[ "$arg" == "$cwd"/* ]]; then
|
|
path_args+=("${arg#"$cwd"/}")
|
|
else
|
|
path_args+=("$arg")
|
|
fi
|
|
done
|
|
|
|
if [[ "$config_given" == false ]]; then
|
|
vale_args+=(--config "$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini")
|
|
fi
|
|
|
|
if [[ ${#path_args[@]} -eq 0 ]]; then
|
|
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
|
# block waiting on a pipe that will never carry content.
|
|
exec vale ${vale_args[@]+"${vale_args[@]}"} < /dev/null
|
|
fi
|
|
|
|
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
|
# is a GNU extension the BSD realpath on macOS doesn't have — and every dest
|
|
# below is a path that doesn't exist yet. python3 is already a hard dependency.
|
|
abspath() {
|
|
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
|
|
}
|
|
|
|
flatten() {
|
|
# Two call shapes: `flatten src dest` (dest already resolved and inside the
|
|
# scratch tree — the per-markdown-file calls in the directory branch below)
|
|
# writes straight to `dest`. `flatten src raw_dest tmpdir` (the single-file
|
|
# branch further down) additionally resolves `raw_dest` the way a separate
|
|
# `abspath` call used to, applies the same sandbox-escape guard, and prints
|
|
# the resolved path — folding two python3 spawns per file into one.
|
|
python3 - "$@" <<'PYTHON'
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
src, dest_input = sys.argv[1], sys.argv[2]
|
|
tmpdir = sys.argv[3] if len(sys.argv) > 3 else None
|
|
|
|
if tmpdir is None:
|
|
dest = dest_input
|
|
else:
|
|
dest = os.path.abspath(dest_input)
|
|
if not dest.startswith(tmpdir + os.sep):
|
|
print(
|
|
f"vale-wrap.sh: refusing to lint '{src}': its scratch copy would "
|
|
f"land outside {tmpdir}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
|
|
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
|
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
|
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
|
content = fh.read()
|
|
|
|
# YAML 1.2 double-quoted escapes (spec 5.7 / 7.3.1). `\<newline>` is handled
|
|
# separately in unescape_double because it also swallows the next indentation.
|
|
DQ_ESCAPES = {
|
|
'0': '\0', 'a': '\a', 'b': '\b', 't': '\t', '\t': '\t', 'n': '\n',
|
|
'v': '\v', 'f': '\f', 'r': '\r', 'e': '\x1b', ' ': ' ', '"': '"',
|
|
'/': '/', '\\': '\\', 'N': '\x85', '_': '\xa0', 'L': '\u2028',
|
|
'P': '\u2029',
|
|
}
|
|
|
|
# First characters that make a plain (unquoted) scalar mean something other than
|
|
# text: YAML's c-indicator set.
|
|
PLAIN_UNSAFE_FIRST = '-?:,[]{}#&*!|>\'"%@`'
|
|
|
|
|
|
def unescape_double(text):
|
|
"""Decode a double-quoted YAML scalar's body to the string YAML parses."""
|
|
out = []
|
|
i = 0
|
|
while i < len(text):
|
|
char = text[i]
|
|
if char != '\\':
|
|
out.append(char)
|
|
i += 1
|
|
continue
|
|
i += 1
|
|
if i >= len(text):
|
|
break
|
|
esc = text[i]
|
|
if esc == '\n':
|
|
i += 1
|
|
while i < len(text) and text[i] in ' \t':
|
|
i += 1
|
|
continue
|
|
if esc in 'xuU':
|
|
width = {'x': 2, 'u': 4, 'U': 8}[esc]
|
|
digits = text[i + 1:i + 1 + width]
|
|
if len(digits) == width:
|
|
try:
|
|
out.append(chr(int(digits, 16)))
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
i += 1 + width
|
|
continue
|
|
out.append(DQ_ESCAPES.get(esc, esc))
|
|
i += 1
|
|
return ''.join(out)
|
|
|
|
|
|
def close_quote(text, quote):
|
|
"""Index of the closing `quote` in `text`, which starts just past the
|
|
opening one. None while the scalar is still unterminated."""
|
|
i = 0
|
|
while i < len(text):
|
|
char = text[i]
|
|
if quote == '"' and char == '\\':
|
|
i += 2
|
|
continue
|
|
if char == quote:
|
|
if quote == "'" and text[i + 1:i + 2] == "'":
|
|
i += 2
|
|
continue
|
|
return i
|
|
i += 1
|
|
return None
|
|
|
|
|
|
def continuation_lines(rest):
|
|
"""Yield the physical lines of `rest` that continue the value started on the
|
|
`description:` line. Indentation-based and blank-line-tolerant, per YAML:
|
|
a blank line (any amount of whitespace) always stays inside; the indent is
|
|
set by the first content line; the value ends at the first line indented
|
|
less than that, at any line flush with the key (that is the next mapping
|
|
key, not a continuation), or at EOF."""
|
|
indent = None
|
|
for line in rest.splitlines(keepends=True):
|
|
text = line.rstrip('\n')
|
|
if text.strip() == '':
|
|
yield line
|
|
continue
|
|
line_indent = len(text) - len(text.lstrip(' \t'))
|
|
if line_indent == 0:
|
|
return
|
|
if indent is None:
|
|
indent = line_indent
|
|
elif line_indent < indent:
|
|
return
|
|
yield line
|
|
|
|
|
|
def emit(value):
|
|
"""Render `value` as a YAML scalar whose source text spells the value out
|
|
verbatim. Vale locates the description by matching the parsed value back
|
|
against the source, so a scalar carrying any escape — `''` in a
|
|
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
|
whole `text.frontmatter.description` scope vanish, the same failure this
|
|
script exists to work around. Verbatim forms only, therefore, tried in
|
|
descending order of fidelity. The first three occupy one physical line; the
|
|
`|-` fallback occupies two, which the caller accounts for when padding."""
|
|
if (value
|
|
and value[0] not in PLAIN_UNSAFE_FIRST
|
|
and ': ' not in value
|
|
and not value.endswith(':')
|
|
and ' #' not in value):
|
|
return value # plain: nothing needs escaping at all
|
|
if "'" not in value:
|
|
return "'" + value + "'" # single-quoted: only `'` would escape
|
|
if '"' not in value and '\\' not in value:
|
|
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
|
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
|
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
|
|
# literal block can — a block scalar's body has no escape syntax at all, so
|
|
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
|
|
# the description scope against it (the header above says the same of the
|
|
# `|` blocks this script deliberately leaves alone; verified against vale
|
|
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
|
|
# value is exactly `value` with no trailing newline.
|
|
return '|-\n ' + value
|
|
|
|
|
|
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
|
if fm_match:
|
|
fm = fm_match.group(2)
|
|
header_m = re.search(r'^description:[ \t]*', fm, re.MULTILINE)
|
|
else:
|
|
header_m = None
|
|
|
|
if header_m:
|
|
head_start = header_m.start()
|
|
value_start = header_m.end()
|
|
header_end = fm.find('\n', value_start)
|
|
header_end = len(fm) if header_end == -1 else header_end
|
|
first = fm[value_start:header_end]
|
|
body_start = header_end + 1
|
|
indicator = first.rstrip()
|
|
|
|
block_m = re.fullmatch(r'([|>])([+-]?[0-9]*|[0-9]*[+-]?)', indicator)
|
|
if block_m and block_m.group(1) == '|':
|
|
kind = None # literal blocks keep their line breaks; vale is fine
|
|
elif block_m:
|
|
kind = 'block' # folded (`>`): the value starts on the next line
|
|
elif indicator == '':
|
|
kind = 'block' # bare `description:`: a plain scalar on later lines
|
|
elif first[:1] == '"':
|
|
kind = 'double'
|
|
elif first[:1] == "'":
|
|
kind = 'single'
|
|
elif first[:1] in '#&*!':
|
|
kind = None # comment, anchor, alias or tag — not a plain scalar
|
|
else:
|
|
kind = 'plain'
|
|
|
|
text = ''
|
|
value_end = value_start
|
|
value_lines = 0
|
|
if kind in ('block', 'plain'):
|
|
body = ''.join(continuation_lines(fm[body_start:]))
|
|
value_end = body_start + len(body)
|
|
if kind == 'block':
|
|
text = body
|
|
value_lines = body.count('\n')
|
|
else:
|
|
text = fm[value_start:value_end]
|
|
value_lines = 1 + body.count('\n')
|
|
if ' #' in text or text.lstrip().startswith('#'):
|
|
# A `#` opens a comment inside a plain scalar. Folding it in
|
|
# would lint text YAML never treats as part of the value, so
|
|
# leave the file alone rather than lint the wrong string.
|
|
kind = None
|
|
elif kind in ('double', 'single'):
|
|
quote = '"' if kind == 'double' else "'"
|
|
inner_start = value_start + 1
|
|
acc = fm[inner_start:body_start]
|
|
idx = close_quote(acc, quote)
|
|
lines = continuation_lines(fm[body_start:])
|
|
while idx is None:
|
|
try:
|
|
acc += next(lines)
|
|
except StopIteration:
|
|
break
|
|
idx = close_quote(acc, quote)
|
|
if idx is None:
|
|
kind = None # unterminated quote: invalid YAML, leave it to vale
|
|
else:
|
|
inner = acc[:idx]
|
|
value_end = inner_start + idx + 1
|
|
text = unescape_double(inner) if quote == '"' else inner.replace("''", "'")
|
|
value_lines = 1 + inner.count('\n')
|
|
|
|
flat = re.sub(r'\s+', ' ', text).strip()
|
|
if kind and flat and value_lines >= 2:
|
|
# `value_end` can land mid-line, just past a closing quote, so extend to
|
|
# the end of that physical line and carry whatever follows (a trailing
|
|
# comment) across unchanged.
|
|
if value_end > 0 and fm[value_end - 1] == '\n':
|
|
span_end = value_end
|
|
trailer = ''
|
|
else:
|
|
newline = fm.find('\n', value_end)
|
|
span_end = len(fm) if newline == -1 else newline + 1
|
|
trailer = fm[value_end:span_end].rstrip('\n')
|
|
scalar = emit(flat)
|
|
# A trailing comment carried across from the original line stays on the
|
|
# `description:` line itself: after a block scalar's `|-` header it is
|
|
# still a comment, but inside the block body it would become part of the
|
|
# value.
|
|
head, newline_sep, block_body = scalar.partition('\n')
|
|
# The replacement displaces the whole span, so the blank-line pad makes
|
|
# up the difference between the lines it displaced and the lines it
|
|
# occupies — every later line number is unchanged. That is one line for
|
|
# the three inline forms and two for the `|-` block; the span itself is
|
|
# at least two lines here (`value_lines >= 2` is a precondition), so the
|
|
# pad count never goes negative.
|
|
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
|
|
new_fm = (fm[:head_start] + 'description: ' + head + trailer
|
|
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
|
|
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
|
+ content[fm_match.end():])
|
|
|
|
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
|
fh.write(content)
|
|
|
|
if tmpdir is not None:
|
|
print(dest)
|
|
PYTHON
|
|
}
|
|
|
|
tmpdir="$(cd "$(mktemp -d)" && pwd -P)"
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
# Mirror of the caller's cwd inside the scratch tree; relative path arguments
|
|
# are resolved from here.
|
|
mirror="$tmpdir$cwd"
|
|
mkdir -p "$mirror"
|
|
|
|
argv_paths=()
|
|
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
|
if [[ "$arg" == /* ]]; then
|
|
raw_dest="$tmpdir$arg"
|
|
else
|
|
raw_dest="$mirror/$arg"
|
|
fi
|
|
if [[ -d "$arg" ]]; then
|
|
dest="$(abspath "$raw_dest")"
|
|
# A path argument with enough leading `..` to climb past the mirror root would
|
|
# write outside the scratch dir. The real filesystem clamps such a path at
|
|
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
|
case "$dest" in
|
|
"$tmpdir"/*) ;;
|
|
*)
|
|
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
mkdir -p "$(dirname "$dest")"
|
|
# A directory is mirrored whole — vale applies its own format filtering to
|
|
# the tree, so any file dropped here would be silently unlinted — and then
|
|
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
|
# vale never lints it and copying it can dwarf the rest of the tree.
|
|
# `find -L` follows symlinks because vale does: it lints both a symlinked
|
|
# file and a file under a symlinked directory, and a bare `-type f` walk
|
|
# would report "0 files" where bare vale reports one. (A symlink loop makes
|
|
# `find` warn on stderr and carry on, which is also what vale does.) The
|
|
# second walk needs no `-L`: the mirror is all real files by construction.
|
|
mkdir -p "$dest"
|
|
while IFS= read -r -d '' rel; do
|
|
mkdir -p "$dest/$(dirname "$rel")"
|
|
cp "$arg/$rel" "$dest/$rel"
|
|
done < <(cd "$arg" && find -L . -name .git -prune -o -type f -print0)
|
|
while IFS= read -r -d '' md; do
|
|
flatten "$md" "$md"
|
|
done < <(find "$dest" -type f -name '*.md' -print0)
|
|
else
|
|
# `abspath` + `flatten` folded into one python3 process — see the comment
|
|
# atop `flatten` above.
|
|
dest="$(flatten "$arg" "$raw_dest" "$tmpdir")"
|
|
fi
|
|
if [[ "$arg" == /* ]]; then
|
|
argv_paths+=("$dest")
|
|
else
|
|
argv_paths+=("$arg")
|
|
fi
|
|
done
|
|
|
|
cd "$mirror"
|
|
vale ${vale_args[@]+"${vale_args[@]}"} ${argv_paths[@]+"${argv_paths[@]}"}
|