Skip to content

Instantly share code, notes, and snippets.

@mennucc
Last active June 8, 2026 12:38
Show Gist options
  • Select an option

  • Save mennucc/88066a3363ebc4da45bdd595451227db to your computer and use it in GitHub Desktop.

Select an option

Save mennucc/88066a3363ebc4da45bdd595451227db to your computer and use it in GitHub Desktop.
parse for latex overfulls

LaTeX Overfull Parser

latex_overfull_parser.py reads a LaTeX .log file and lists Overfull \hbox warnings with the source file, source line, amount in points, and log line.

Usage

Run it from the directory containing the LaTeX source and log:

./latex_overfull_parser.py paper.log

If no log file is supplied, the script scans the current directory for .tex files that have a matching .log file. If exactly one pair is found, that log is used automatically.

./latex_overfull_parser.py

Useful options:

./latex_overfull_parser.py --min 5 paper.log
./latex_overfull_parser.py -v paper.log
  • --min N reports only overfull boxes wider than N points.
  • -v prints a small source excerpt around each reported line.

Better Source Tracking

The parser works best when the LaTeX source uses:

\usepackage{structuredlog}

With structuredlog, the log contains explicit source-file markers, so the script can attribute warnings more reliably across \input and \include files. Without it, the script falls back to parsing ordinary LaTeX file-open markers, which is less robust for complex documents.

#!/usr/bin/env python3
"""List overfull boxes from a .log file with source file and line."""
from __future__ import annotations
import argparse
import pathlib
import re
from dataclasses import dataclass
@dataclass
class Overfull:
amount: float
file: str
line_info: str
log_line: int
OPEN_PAT = re.compile(r"\(\./([^()\s]+\.tex)")
OVER_PAT = re.compile(
r"Overfull \\hbox \(([-0-9.]+)pt too wide\) "
r"(?:in paragraph at lines ([0-9]+)--([0-9]+)|detected at line ([0-9]+))"
)
def parse_log(path: pathlib.Path) -> list[Overfull]:
lines = path.read_text(encoding="latin-1", errors="ignore").splitlines()
current: str | None = None
items: list[Overfull] = []
default_source = path.with_suffix(".tex").name
use_structured = False
level_stack: list[tuple[int, str]] = []
struct_pat = re.compile(r"^=+ *\(LEVEL *(\d+) *(START|STOP)\) (.+)$")
for idx, line in enumerate(lines, 1):
if "Package: structuredlog" in line:
use_structured = True
if use_structured:
stripped = line.strip()
m = struct_pat.match(stripped)
if m:
level = int(m.group(1))
action = m.group(2)
name = m.group(3).strip()
if action == "START":
level_stack.append((level, name))
else:
# pop matching level or higher
while level_stack and level_stack[-1][0] >= level:
level_stack.pop()
current = level_stack[-1][1] if level_stack else None
else:
matches = OPEN_PAT.findall(line)
if matches:
filtered = []
for name in matches:
if f"(./{name})" in line:
continue
filtered.append(name)
if filtered:
current = filtered[-1]
match = OVER_PAT.search(line)
if match:
amount = float(match.group(1))
line_info = match.group(4) if match.group(4) else f"{match.group(2)}--{match.group(3)}"
items.append(
Overfull(
amount=amount,
file=current or default_source,
line_info=line_info,
log_line=idx,
)
)
return items
def load_context(path: pathlib.Path, line_info: str, radius: int = 2) -> list[str]:
try:
content = path.read_text(encoding="utf-8").splitlines()
except FileNotFoundError:
return [f"(file not found: {path})"]
except UnicodeDecodeError:
content = path.read_text(encoding="latin-1", errors="ignore").splitlines()
if "--" in line_info:
start_str, end_str = line_info.split("--", 1)
start = int(start_str)
end = int(end_str)
else:
start = end = int(line_info)
start = max(1, start - radius)
end = min(len(content), end + radius)
snippet = []
for idx in range(start, end + 1):
snippet.append(f"> {idx:>4}: {content[idx - 1]}")
return snippet
def infer_log_path(parser: argparse.ArgumentParser) -> pathlib.Path:
candidates = sorted(
tex.with_suffix(".log")
for tex in pathlib.Path.cwd().glob("*.tex")
if tex.with_suffix(".log").is_file()
)
if len(candidates) == 1:
return candidates[0]
if not candidates:
parser.error("no log argument supplied and no .tex file with a matching .log was found")
formatted = ", ".join(str(path) for path in candidates)
parser.error(
"no log argument supplied and more than one .tex file with a matching "
f".log was found: {formatted}"
)
def main() -> None:
parser = argparse.ArgumentParser(description="List overfull boxes from a .log file.")
parser.add_argument("log", nargs="?", help="Path to the log file.")
parser.add_argument("--min", type=float, default=20.0, help="Minimum threshold in pt.")
parser.add_argument("-v", action="store_true", help="Show context from the source file.")
parser.add_argument("--debug", action="store_true", help="Print structuredlog parsing debug output.")
args = parser.parse_args()
log_path = pathlib.Path(args.log) if args.log else infer_log_path(parser)
items = [item for item in parse_log(log_path) if item.amount > args.min]
items.sort(key=lambda x: x.amount, reverse=True)
if not items:
print("No overfull boxes above the threshold.")
return
for item in items:
if "--" in item.line_info:
line_out = item.line_info.split("--", 1)[0]
else:
line_out = item.line_info
print(
f"{item.file}:{line_out}: "
f"{item.amount:.2f}pt overfull (log line {item.log_line})"
)
if args.v and item.file != "UNKNOWN":
for line in load_context(pathlib.Path(item.file), item.line_info):
print(line)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment