#!/usr/bin/env python3 """One exercise sheet per student, with their prefix filled in, in one PDF. python3 build-class.py 100 # students 1..100 -> class.pdf python3 build-class.py 100 -o out.pdf python3 build-class.py 5 12 -o few.pdf # students 5..12 Student n gets 2a0d:d741:::/48; the lab /40 allows 1..255. Each sheet is compiled separately and must come out to exactly two pages, so the result prints double-sided with one student per leaf. Needs pdflatex and pdfunite (poppler). """ import argparse import os import shutil import subprocess import sys import tempfile HERE = os.path.dirname(os.path.abspath(__file__)) def page_count(pdf): out = subprocess.run(["pdfinfo", pdf], capture_output=True, text=True, check=True).stdout return int(next(line.split()[-1] for line in out.splitlines() if line.startswith("Pages:"))) def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("first", type=int, help="first student number, or the count if it is the only number") ap.add_argument("last", type=int, nargs="?") ap.add_argument("-o", "--output", default=os.path.join(HERE, "class.pdf")) args = ap.parse_args() first, last = (1, args.first) if args.last is None else (args.first, args.last) if not 1 <= first <= last <= 255: sys.exit("student numbers must be within 1..255: the lab /40 has 255 usable /48s") with tempfile.TemporaryDirectory() as tmp: for name in os.listdir(HERE): if name.endswith((".tex", ".cls", ".sty", ".bst", ".bib")): shutil.copy(os.path.join(HERE, name), tmp) parts = [] for n in range(first, last + 1): job = f"student-{n:03d}" tex = f"\\def\\studentnum{{{n}}}\\def\\studenthex{{{n:x}}}\\input{{handout.tex}}" for _ in range(2): # twice, for the figure and table references r = subprocess.run(["pdflatex", "-interaction=nonstopmode", "-halt-on-error", "-jobname", job, tex], cwd=tmp, capture_output=True, text=True) if r.returncode != 0: sys.exit(f"student {n}: pdflatex failed:\n{r.stdout[-2000:]}") pdf = os.path.join(tmp, job + ".pdf") pages = page_count(pdf) if pages != 2: sys.exit(f"student {n}: sheet is {pages} pages, not 2; double-sided printing would misalign") parts.append(pdf) print(f"\rstudent {n}: 2a0d:d741:{n:x}::/48", end="", flush=True) print() subprocess.run(["pdfunite", *parts, args.output], check=True) print(f"wrote {args.output}: students {first}..{last}, {page_count(args.output)} pages") if __name__ == "__main__": main()