mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:07:17 +01:00
Add shared 250-line file-length gate
Files that cannot be read in one piece force re-reads and partial edits, which is the largest avoidable cost in an LLM-assisted workflow. This adds the enforcement side of a 250-line cap covering code AND prose. - file_length/config.py: single source of truth for the cap and its exemptions (generated, markup, vendored, data-ish txt), shared by both the gate and the survey so the two cannot silently disagree. - file_length/check.py: the check itself. Exits 1 and lists violations; adjudication is an exit code, not a judgement call. - scripts/check_file_length.sh: thin wrapper pre-commit calls with staged files. Verified against six fixtures: over-cap code fails, under-cap passes, generated l10n (1417 lines) exempt, vendored pdfjs exempt, the 174800-line wordlist exempt as data, and a 1101-line CLAUDE.md correctly fails since prose is capped too. Baseline at time of writing: 657 files over 250 lines across 29 repos. Each repo carries a refactor_claude_todo.md prompt with its own list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7yZz45ezmzCB89Cv5gAtZ
This commit is contained in:
parent
284aa1e360
commit
c9bf289510
5
file_length/__init__.py
Normal file
5
file_length/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Shared 250-line file-length cap: config, gate, and survey."""
|
||||||
|
|
||||||
|
from file_length.config import MAX_LINES
|
||||||
|
|
||||||
|
__all__ = ["MAX_LINES"]
|
||||||
128
file_length/check.py
Normal file
128
file_length/check.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""Gate: fail if any given file exceeds the 250-line cap.
|
||||||
|
|
||||||
|
Invoked by `scripts/check_file_length.sh`, which pre-commit calls with the
|
||||||
|
staged files. Exits 1 and lists every violation; exits 0 silently otherwise.
|
||||||
|
|
||||||
|
The check is deliberately deterministic code rather than a judgement call --
|
||||||
|
adjudication belongs in an exit code, not in a model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
if __package__ in (None, ""): # invoked as a plain script
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from file_length.config import ( # noqa: E402
|
||||||
|
CAPPED_EXTENSIONS,
|
||||||
|
EXCLUDED_DIRS,
|
||||||
|
MAX_LINES,
|
||||||
|
is_data_text,
|
||||||
|
is_generated,
|
||||||
|
is_vendored,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def count_lines(path: Path) -> int | None:
|
||||||
|
"""Number of lines, or None if the file is binary or unreadable."""
|
||||||
|
try:
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
first = handle.read(8192)
|
||||||
|
if b"\x00" in first:
|
||||||
|
return None
|
||||||
|
count = first.count(b"\n")
|
||||||
|
while chunk := handle.read(1 << 20):
|
||||||
|
count += chunk.count(b"\n")
|
||||||
|
if first and not first.endswith(b"\n") and count == 0:
|
||||||
|
count = 1
|
||||||
|
return count
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exempt_reason(path: Path, lines: int) -> str | None:
|
||||||
|
"""Why this file is not subject to the cap, or None if it is subject."""
|
||||||
|
if path.suffix.lower() not in CAPPED_EXTENSIONS:
|
||||||
|
return "not a capped extension"
|
||||||
|
if is_vendored(path):
|
||||||
|
return "vendored / third-party"
|
||||||
|
if is_generated(path):
|
||||||
|
return "generated"
|
||||||
|
try:
|
||||||
|
size = path.stat().st_size
|
||||||
|
except OSError:
|
||||||
|
return "unreadable"
|
||||||
|
if is_data_text(path, lines, size):
|
||||||
|
return "data-ish text (wordlist)"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def iter_all(root: Path):
|
||||||
|
"""Every file under root, skipping excluded directories."""
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
|
||||||
|
dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS]
|
||||||
|
for name in filenames:
|
||||||
|
candidate = Path(dirpath) / name
|
||||||
|
if not candidate.is_symlink():
|
||||||
|
yield candidate
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=f"Fail if any file exceeds {MAX_LINES} lines."
|
||||||
|
)
|
||||||
|
parser.add_argument("paths", nargs="*", type=Path)
|
||||||
|
parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="check the whole tree under the current directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--explain", action="store_true", help="also report why files were skipped"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.all:
|
||||||
|
targets = list(iter_all(Path.cwd()))
|
||||||
|
elif args.paths:
|
||||||
|
targets = args.paths
|
||||||
|
else:
|
||||||
|
parser.error("give file paths or --all")
|
||||||
|
|
||||||
|
violations: list[tuple[Path, int]] = []
|
||||||
|
for path in targets:
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
lines = count_lines(path)
|
||||||
|
if lines is None:
|
||||||
|
continue
|
||||||
|
reason = exempt_reason(path, lines)
|
||||||
|
if reason is not None:
|
||||||
|
if args.explain:
|
||||||
|
print(f" skip {path}: {reason}")
|
||||||
|
continue
|
||||||
|
if lines > MAX_LINES:
|
||||||
|
violations.append((path, lines))
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"File-length gate FAILED: {len(violations)} file(s) over {MAX_LINES} lines\n",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
for path, lines in sorted(violations, key=lambda item: -item[1]):
|
||||||
|
over = lines - MAX_LINES
|
||||||
|
print(f" {path}: {lines} lines (over by {over})", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
"\nSplit them, or see refactor_claude_todo.md in this repo.", file=sys.stderr
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
203
file_length/config.py
Normal file
203
file_length/config.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
"""Shared configuration for the 250-line file-length cap.
|
||||||
|
|
||||||
|
Single source of truth for the cap and its exemptions, imported by both the
|
||||||
|
pre-commit gate (`check.py`) and the survey. Duplicating these lists in shell
|
||||||
|
is what lets a gate and a survey silently disagree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
#: Hard cap. Files above this fail the gate. Code and prose alike.
|
||||||
|
MAX_LINES = 250
|
||||||
|
|
||||||
|
#: Extensions the cap applies to: real programming languages plus prose.
|
||||||
|
#: Markup (.html/.css/.scss) is deliberately absent -- templates split into
|
||||||
|
#: partials, not functions, so a line cap does not express anything useful.
|
||||||
|
CAPPED_EXTENSIONS = {
|
||||||
|
# code
|
||||||
|
".py",
|
||||||
|
".dart",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".sh",
|
||||||
|
".bash",
|
||||||
|
".zsh",
|
||||||
|
".c",
|
||||||
|
".h",
|
||||||
|
".cpp",
|
||||||
|
".cc",
|
||||||
|
".hpp",
|
||||||
|
".go",
|
||||||
|
".rs",
|
||||||
|
".java",
|
||||||
|
".kt",
|
||||||
|
".kts",
|
||||||
|
".swift",
|
||||||
|
".rb",
|
||||||
|
".lua",
|
||||||
|
".sql",
|
||||||
|
".vue",
|
||||||
|
".cs",
|
||||||
|
".php",
|
||||||
|
# prose
|
||||||
|
".md",
|
||||||
|
".markdown",
|
||||||
|
".txt",
|
||||||
|
".rst",
|
||||||
|
".tex",
|
||||||
|
".org",
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Generated files: regenerated by a tool, so splitting them is meaningless
|
||||||
|
#: (the tool would just rewrite the original next run).
|
||||||
|
GENERATED_PATTERN = re.compile(
|
||||||
|
r"\.g\.dart$"
|
||||||
|
r"|\.freezed\.dart$"
|
||||||
|
r"|\.gr\.dart$"
|
||||||
|
r"|\.config\.dart$"
|
||||||
|
r"|\.mocks\.dart$"
|
||||||
|
r"|\.min\.(js|css)$"
|
||||||
|
r"|\.pb\.(go|py)$"
|
||||||
|
r"|_pb2(_grpc)?\.py$"
|
||||||
|
r"|\.generated\.ts$"
|
||||||
|
r"|\.d\.ts$"
|
||||||
|
r"|/l10n/generated/"
|
||||||
|
r"|/generated/"
|
||||||
|
)
|
||||||
|
|
||||||
|
#: A marker in the first few lines also marks a file generated. Covers tools
|
||||||
|
#: that do not use a distinguishing extension.
|
||||||
|
GENERATED_MARKERS = (
|
||||||
|
"GENERATED CODE - DO NOT MODIFY",
|
||||||
|
"@generated",
|
||||||
|
"DO NOT EDIT",
|
||||||
|
"Autogenerated",
|
||||||
|
"AUTOGENERATED",
|
||||||
|
"generated by",
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Directory names never descended into: dependencies, build output, caches.
|
||||||
|
EXCLUDED_DIRS = frozenset(
|
||||||
|
{
|
||||||
|
".git",
|
||||||
|
"node_modules",
|
||||||
|
".dart_tool",
|
||||||
|
"build",
|
||||||
|
".venv",
|
||||||
|
"venv",
|
||||||
|
"env",
|
||||||
|
"site-packages",
|
||||||
|
"target",
|
||||||
|
"ephemeral",
|
||||||
|
"__pycache__",
|
||||||
|
".mypy_cache",
|
||||||
|
".pytest_cache",
|
||||||
|
".ruff_cache",
|
||||||
|
"dist",
|
||||||
|
".next",
|
||||||
|
"vendor",
|
||||||
|
"third_party",
|
||||||
|
"Pods",
|
||||||
|
".gradle",
|
||||||
|
".idea",
|
||||||
|
".cxx",
|
||||||
|
"coverage",
|
||||||
|
"htmlcov",
|
||||||
|
".tox",
|
||||||
|
"Library",
|
||||||
|
"Temp",
|
||||||
|
"obj",
|
||||||
|
".terraform",
|
||||||
|
".fvm",
|
||||||
|
".angular",
|
||||||
|
".cache",
|
||||||
|
".svelte-kit",
|
||||||
|
"migrations",
|
||||||
|
".ci-mirror-venv",
|
||||||
|
".hippo",
|
||||||
|
".build-deps",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Third-party content living *inside* a repo you own, keyed by repo dir name.
|
||||||
|
#: Verified by git remote / vendor manifest, not guessed.
|
||||||
|
VENDORED_SUBPATHS = {
|
||||||
|
"mcp-servers": ("servers",),
|
||||||
|
"dufs-cloud": ("app/assets/pdfjs", "web/assets/pdfjs"),
|
||||||
|
".claude": (
|
||||||
|
"projects",
|
||||||
|
"shell-snapshots",
|
||||||
|
"cache",
|
||||||
|
"backups",
|
||||||
|
"todos",
|
||||||
|
"statsig",
|
||||||
|
"history",
|
||||||
|
"chrome",
|
||||||
|
"downloads",
|
||||||
|
"plugins",
|
||||||
|
"ide",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Whole repos under ~ that are third-party clones, not kuhy's work.
|
||||||
|
#: Verified 2026-08-14 by `git remote get-url origin` + commit authors.
|
||||||
|
THIRD_PARTY_REPOS = frozenset(
|
||||||
|
{
|
||||||
|
"StringTheory", # github.com/AnthonySf/TheStringTheory
|
||||||
|
"reaper-mcp", # github.com/TwelveTake-Studios/reaper-mcp
|
||||||
|
"freesound-mcp", # github.com/timjrobinson/FreesoundMCPServer
|
||||||
|
"llama_cpp_dart_local", # generated FFI bindings
|
||||||
|
"aseprite-mcp",
|
||||||
|
"aur",
|
||||||
|
"build-your-own-x",
|
||||||
|
"build_your_x",
|
||||||
|
"byox-ladder",
|
||||||
|
"warriorjs",
|
||||||
|
"untools",
|
||||||
|
"awesome-mcp-explorer",
|
||||||
|
"runnerup",
|
||||||
|
"depotdownloader",
|
||||||
|
"actions-runner",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
#: A `.txt` whose mean line is shorter than this is data (a wordlist, a
|
||||||
|
#: coordinate dump), not prose, so the cap does not apply.
|
||||||
|
DATA_TXT_MAX_MEAN_LINE = 25
|
||||||
|
|
||||||
|
|
||||||
|
def is_generated(path: Path) -> bool:
|
||||||
|
"""True if the file is tool-generated and must not be split by hand."""
|
||||||
|
if GENERATED_PATTERN.search(path.as_posix()):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||||
|
head = "".join(next(handle, "") for _ in range(5))
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return any(marker in head for marker in GENERATED_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
|
def is_data_text(path: Path, lines: int, size: int) -> bool:
|
||||||
|
"""True for wordlist-style .txt: many lines, very short ones."""
|
||||||
|
if path.suffix.lower() != ".txt" or lines <= 0:
|
||||||
|
return False
|
||||||
|
return (size / lines) < DATA_TXT_MAX_MEAN_LINE
|
||||||
|
|
||||||
|
|
||||||
|
def is_vendored(path: Path) -> bool:
|
||||||
|
"""True if the path sits under a known vendored subtree or excluded dir."""
|
||||||
|
parts = path.parts
|
||||||
|
if any(part in EXCLUDED_DIRS for part in parts):
|
||||||
|
return True
|
||||||
|
posix = path.as_posix()
|
||||||
|
for repo, subs in VENDORED_SUBPATHS.items():
|
||||||
|
for sub in subs:
|
||||||
|
if f"/{repo}/{sub}/" in posix or posix.startswith(f"{repo}/{sub}/"):
|
||||||
|
return True
|
||||||
|
return any(f"/{repo}/" in posix for repo in THIRD_PARTY_REPOS)
|
||||||
96
refactor_claude_todo.md
Normal file
96
refactor_claude_todo.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Refactor TODO — enforce the 250-line file cap
|
||||||
|
|
||||||
|
> **This file is a ready-to-use prompt.** Paste it to Claude, or open this repo
|
||||||
|
> and say "do refactor_claude_todo". It is self-contained: everything needed to
|
||||||
|
> execute is below. Generated 2026-08-14 from a measured survey of every repo.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Every file in this repo must be **at most 250 lines** — source, tests, and
|
||||||
|
prose (`.md`/`.txt`/`.rst`/`.tex`) alike — and must **stay** that way forever,
|
||||||
|
enforced by a gate that fails the commit, not by a note anyone can ignore.
|
||||||
|
|
||||||
|
Why: a file that cannot be read in one piece forces re-reads and partial edits,
|
||||||
|
which is the single largest avoidable cost in an LLM-assisted workflow. Aim by
|
||||||
|
churn, not size alone — refactoring pays where code is read and changed often
|
||||||
|
(Fowler, *refactoring economic benefit*).
|
||||||
|
|
||||||
|
## Scope in this repo
|
||||||
|
|
||||||
|
- **39 files** currently exceed 250 lines (of 139 eligible files).
|
||||||
|
- **14,348 lines** sit in violation; longest file is **525 lines**.
|
||||||
|
|
||||||
|
Exempt (do NOT split these):
|
||||||
|
- generated files — `*.g.dart`, `*.freezed.dart`, `*.gr.dart`, `**/l10n/generated/**`,
|
||||||
|
anything with a `GENERATED` header
|
||||||
|
- markup — `.html`, `.css`, `.scss`
|
||||||
|
- data files — `.json`, `.yaml`, `.csv`, wordlists and other data-ish `.txt`
|
||||||
|
(mean line length under 25 chars)
|
||||||
|
|
||||||
|
## Violations, highest ROI first
|
||||||
|
|
||||||
|
ROI = lines x commits in the last year. Work top-down; a long file nobody edits
|
||||||
|
has near-zero payoff and should not be first.
|
||||||
|
|
||||||
|
| lines | commits/yr | kind | file |
|
||||||
|
|------:|-----------:|:-----|:-----|
|
||||||
|
| 515 | 6 | code | `gatelock/gatelock/_window.py` |
|
||||||
|
| 477 | 3 | code | `gatelock/gatelock/tests/test_recovery.py` |
|
||||||
|
| 259 | 5 | code | `crdt_sync_dart/lib/src/sync.dart` |
|
||||||
|
| 406 | 3 | code | `crdt-sync/crdt_sync/_firebase_auth.py` |
|
||||||
|
| 393 | 3 | code | `crdt-sync/crdt_sync/tests/test_firebase_auth.py` |
|
||||||
|
| 363 | 3 | code | `crdt-sync/crdt_sync/_sync.py` |
|
||||||
|
| 525 | 2 | code | `crdt_sync_dart/test/firebase_auth_rest_test.dart` |
|
||||||
|
| 483 | 2 | code | `gatelock/gatelock/_scrollable.py` |
|
||||||
|
| 321 | 3 | code | `gatelock/gatelock/_recovery.py` |
|
||||||
|
| 457 | 2 | code | `sync_settings_ui/test/sync_settings_screen_test.dart` |
|
||||||
|
| 452 | 2 | code | `crdt_sync_dart/test/mirror_store_test.dart` |
|
||||||
|
| 451 | 2 | code | `crdt_sync_dart/test/sync_revisions_test.dart` |
|
||||||
|
| 439 | 2 | code | `aur-publish/publish.sh` |
|
||||||
|
| 291 | 3 | code | `crdt_sync_dart/test/github_client_test.dart` |
|
||||||
|
| 418 | 2 | code | `gatelock/gatelock/tests/test_outputs.py` |
|
||||||
|
|
||||||
|
_(24 further files over 250 lines not listed — re-run the survey for the full set.)_
|
||||||
|
|
||||||
|
## How to split
|
||||||
|
|
||||||
|
- **Python** — extract cohesive helpers into sibling modules; keep the public
|
||||||
|
API and imports stable.
|
||||||
|
- **Shell** — split into `lib/*.sh` sourced by a thin entry script. Keep
|
||||||
|
`set -euo pipefail` in each.
|
||||||
|
- **Dart / TypeScript** — extract widgets/components into their own files.
|
||||||
|
- **Tests** — split by test-group into sibling files
|
||||||
|
(`foo.test.ts` -> `foo.parsing.test.ts`, `foo.render.test.ts`). Coverage must
|
||||||
|
not drop.
|
||||||
|
- **Docs** — split into topic files under `docs/` with an index. For an
|
||||||
|
oversized `CLAUDE.md`, move detail into referenced docs so the
|
||||||
|
always-loaded part shrinks.
|
||||||
|
|
||||||
|
**Do not** game the cap: no one-lining, no deleting tests, no moving code into
|
||||||
|
an exempt extension, no `# noqa`-style suppressions.
|
||||||
|
|
||||||
|
## Make it permanent (required — this is the point)
|
||||||
|
|
||||||
|
A refactor without a gate silently regrows. Before this task is done:
|
||||||
|
|
||||||
|
1. Wire the shared gate `~/utils/scripts/check_file_length.sh` into this repo's
|
||||||
|
`.pre-commit-config.yaml` as a local hook. If the repo has no pre-commit
|
||||||
|
config, add a minimal one.
|
||||||
|
2. The hook checks **files in the commit** (not the whole tree), so unrelated
|
||||||
|
commits never break, and it **fails** — exit 1, not a warning.
|
||||||
|
3. No baseline file and no allowlist. Those are suppressions.
|
||||||
|
4. If this repo has CI (`.github/workflows`), add the same check there so it
|
||||||
|
also fails on push.
|
||||||
|
|
||||||
|
## Done condition
|
||||||
|
|
||||||
|
- `bash ~/utils/scripts/check_file_length.sh --all` from this repo root exits 0.
|
||||||
|
- The repo's own test suite and coverage bar are still green.
|
||||||
|
- `pre-commit run --files <changed files>` passes.
|
||||||
|
- A deliberately over-250-line test file, staged, makes `git commit` **fail**.
|
||||||
|
- For a deployed daemon/app: the entry point still actually runs.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
Run the repo's own suite, then run the program itself and confirm the
|
||||||
|
behaviour is unchanged. Testing is the last step, never the first.
|
||||||
38
scripts/check_file_length.sh
Executable file
38
scripts/check_file_length.sh
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Fail if any file exceeds the shared 250-line cap.
|
||||||
|
#
|
||||||
|
# A file that cannot be read in one piece forces re-reads and partial edits,
|
||||||
|
# which is the largest avoidable cost in an LLM-assisted workflow. The cap
|
||||||
|
# applies to code AND prose; generated files, markup and data are exempt.
|
||||||
|
#
|
||||||
|
# The real check lives in Python (file_length/check.py) so that the gate and
|
||||||
|
# the survey share one exemption list -- see rules/shell.instructions.md on
|
||||||
|
# never embedding another language's logic inline.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/check_file_length.sh <file> [<file> ...] # pre-commit passes these
|
||||||
|
# scripts/check_file_length.sh --all # whole tree, from cwd
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly UTILS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
readonly CHECKER="$UTILS_ROOT/file_length/check.py"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
echo "Usage: $(basename "$0") <file>... | --all" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$CHECKER" ]]; then
|
||||||
|
echo "Error: checker not found at $CHECKER" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PYTHONPATH="$UTILS_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
||||||
|
python3 "$CHECKER" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
Reference in New Issue
Block a user