mirror of
https://github.com/4jcraft/4jcraft.git
synced 2026-04-25 14:33:35 +00:00
184 lines
5.1 KiB
Python
184 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
|
|
DEFAULT_EXTENSIONS = {
|
|
".c",
|
|
".cc",
|
|
".cpp",
|
|
".cxx",
|
|
".h",
|
|
".hh",
|
|
".hpp",
|
|
".hxx",
|
|
".inl",
|
|
".ipp",
|
|
}
|
|
DEFAULT_JOBS = os.cpu_count() or 4
|
|
DEFAULT_TIMEOUT = 10
|
|
|
|
# ansi escapes
|
|
RESET = "\033[0m" if sys.stdout.isatty() else ""
|
|
GREEN = "\033[32m" if sys.stdout.isatty() else ""
|
|
YELLOW = "\033[33m" if sys.stdout.isatty() else ""
|
|
RED = "\033[31m" if sys.stdout.isatty() else ""
|
|
BOLD = "\033[1m" if sys.stdout.isatty() else ""
|
|
|
|
def format_file(path, clang_format, extra_args, timeout):
|
|
cmd = [clang_format, "-i"]
|
|
cmd += extra_args
|
|
cmd.append(str(path))
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
if result.returncode != 0:
|
|
msg = (result.stderr or result.stdout or "non-zero exit code").strip()
|
|
return ("error", str(path), msg)
|
|
return ("ok", str(path), None)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return ("timeout", str(path), f"exceeded {timeout}s timeout")
|
|
|
|
except FileNotFoundError:
|
|
return ("error", str(path), f"'{clang_format}' not found — is it installed?")
|
|
|
|
except Exception as exc:
|
|
return ("error", str(path), str(exc))
|
|
|
|
def find_files(root, extensions, exclude_dirs):
|
|
found = []
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
|
|
for fname in filenames:
|
|
if Path(fname).suffix.lower() in extensions:
|
|
found.append(Path(dirpath) / fname)
|
|
return sorted(found)
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(
|
|
description="Recursively runs clang-format on C/C++ files.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
p.add_argument("directory", nargs="?", default=".", type=Path)
|
|
p.add_argument(
|
|
"--clang-format",
|
|
default="clang-format",
|
|
metavar="BIN",
|
|
help="Path to the clang-format executable. (default: clang-format)",
|
|
)
|
|
p.add_argument(
|
|
"-j",
|
|
"--jobs",
|
|
type=int,
|
|
default=DEFAULT_JOBS,
|
|
metavar="N",
|
|
help=f"Number of parallel workers. (default: {DEFAULT_JOBS})",
|
|
)
|
|
p.add_argument(
|
|
"--timeout",
|
|
type=int,
|
|
default=DEFAULT_TIMEOUT,
|
|
metavar="SECS",
|
|
help=f"Per-file timeout in seconds. (default: {DEFAULT_TIMEOUT})",
|
|
)
|
|
p.add_argument(
|
|
"--extensions",
|
|
nargs="+",
|
|
metavar="EXT",
|
|
help="File extensions to process (including the dot)."
|
|
f"(default: {' '.join(sorted(DEFAULT_EXTENSIONS))})",
|
|
)
|
|
p.add_argument(
|
|
"--exclude",
|
|
nargs="*",
|
|
default=[],
|
|
metavar="DIR",
|
|
help="Directory names to skip during traversal (e.g. build third_party).",
|
|
)
|
|
p.add_argument(
|
|
"--clang-format-args",
|
|
nargs=argparse.REMAINDER,
|
|
default=[],
|
|
metavar="...",
|
|
)
|
|
args = p.parse_args()
|
|
|
|
root = args.directory.resolve()
|
|
if not root.is_dir():
|
|
print(f"{RED}Error:{RESET} '{root}' is not a directory.", file=sys.stderr)
|
|
return 1
|
|
|
|
extensions = (
|
|
{e if e.startswith(".") else f".{e}" for e in args.extensions}
|
|
if args.extensions
|
|
else DEFAULT_EXTENSIONS
|
|
)
|
|
|
|
exclude_dirs = set(args.exclude)
|
|
|
|
files = find_files(root, extensions, exclude_dirs)
|
|
if not files:
|
|
print(f"{YELLOW}No source files found.{RESET}")
|
|
return 0
|
|
|
|
total = len(files)
|
|
print(f"Found {BOLD}{total}{RESET} file(s).")
|
|
|
|
counters = {"ok": 0, "timeout": 0, "error": 0}
|
|
completed = 0
|
|
|
|
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
|
futures = {
|
|
pool.submit(
|
|
format_file,
|
|
f,
|
|
args.clang_format,
|
|
args.clang_format_args,
|
|
args.timeout,
|
|
): f
|
|
for f in files
|
|
}
|
|
|
|
for future in as_completed(futures):
|
|
status, path_str, error = future.result()
|
|
counters[status] += 1
|
|
completed += 1
|
|
rel = os.path.relpath(path_str, root)
|
|
|
|
if status == "ok":
|
|
tag = f"{GREEN}[ OK ]{RESET}"
|
|
elif status == "timeout":
|
|
tag = f"{YELLOW}[ TIMEOUT ]{RESET}"
|
|
else:
|
|
tag = f"{RED}[ ERROR ]{RESET}"
|
|
|
|
progress = f"[{completed:>{len(str(total))}}/{total}]"
|
|
line = f"{progress} {tag} {rel}"
|
|
if error:
|
|
line += f"\n {RED}{error}{RESET}"
|
|
print(line)
|
|
|
|
print()
|
|
print(f"{BOLD}Summary{RESET}")
|
|
print(f"Total: {total}")
|
|
print(f"{GREEN}OK: {counters['ok']}{RESET}")
|
|
|
|
if counters["timeout"]:
|
|
print(f"{YELLOW}Timeout: {counters['timeout']}{RESET}")
|
|
if counters["error"]:
|
|
print(f"{RED}Error: {counters['error']}{RESET}")
|
|
|
|
return 1 if (counters["timeout"] or counters["error"]) else 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|