TeXLiveに含まれるLaTeX packageの一覧を得る
Denoで作ったCLI by GPT-5-mini
code:tlpdb-graph.ts
import { parseTlpdb } from "./parse_tlpdb.ts";
import { printDotForRoots, printTreeForest } from "./graph.ts";
import { parseArgs } from "jsr:@std/cli@1/parse-args";
const DEFAULT_URL =
function usage() {
console.log(`tlpdb-deps (Deno)
Usage:
Options:
--file=<path> Read texlive.tlpdb from local file instead of fetching
--url=<url> Read texlive.tlpdb from URL (default: ${DEFAULT_URL})
--format=tree|dot Output format: tree (default) or dot
--depth=<n> Maximum recursion depth for tree (default: 0, 0 = unlimited)
--reverse Show reverse-dependencies (packages that depend on the given ones)
--minimal Print minimal explicit add list from given packages
Notes:
- Multiple <package> arguments are treated as depth-0 roots and printed as a forest in tree mode.
- With --minimal, outputs only those given packages that are not dependencies of any other given package.
--help Show this help
Examples:
deno run --allow-net main.ts latex --format=tree
deno run --allow-read main.ts --file=./texlive.tlpdb babel --format=dot > babel.dot
`);
}
async function loadTlpdbText(opts: { file?: string; url?: string }) {
if (opts.file) {
return await Deno.readTextFile(opts.file);
} else {
const url = opts.url ?? DEFAULT_URL;
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(
failed to fetch ${url}: ${resp.status} ${resp.statusText},
);
}
return await resp.text();
}
}
async function main() {
const rawArgs = Deno.args;
const args = parseArgs(rawArgs, {
default: { format: "tree", depth: "0", reverse: false },
});
if (args.help) {
usage();
return;
}
const pkgs: string[] = args._.map(String);
if (pkgs.length === 0) {
console.error("Error: at least one package name must be given.\n");
usage();
Deno.exit(2);
}
const file = args.file as string | undefined;
const url = args.url as string | undefined;
const format = (args.format as string).toLowerCase();
let depth = parseInt(args.depth as string, 10);
if (isNaN(depth) || depth < 0) depth = 10;
const reverse = Boolean(args.reverse);
const minimal = Boolean(args.minimal);
try {
const txt = await loadTlpdbText({ file, url });
const pkgsMap = parseTlpdb(txt); // Map<string, { name, fields, depends }>
// Build forward adjacency map (only package names that exist in tlpdb are included).
const adjacencyFwd: Map<string, string[]> = new Map();
adjacencyFwd.set(name, entry.depends ?? []);
}
// Determine which adjacency to use for graph printing
let adjacency: Map<string, string[]> = adjacencyFwd;
if (reverse) {
// Build reverse map
const rev: Map<string, string[]> = new Map();
for (const k of adjacencyFwd.keys()) rev.set(k, [] as string[]);
for (const p, deps of adjacencyFwd.entries()) { for (const d of deps) {
if (!rev.has(d)) rev.set(d, [] as string[]);
rev.get(d)!.push(p);
}
}
adjacency = rev;
}
// Validate requested packages
const missing = pkgs.filter((p: string) => !pkgsMap.has(p));
if (missing.length > 0) {
console.error("Warning: the following packages were not found in tlpdb:");
for (const m of missing) console.error(" - " + m);
// continue anyway; nodes missing will be shown as leaf nodes (no deps known)
}
if (minimal) {
// Compute minimal explicit add list from given packages.
// If A depends on B and both are given, we can drop B.
const given = Array.from(new Set(pkgs));
// Build reachability from each given root
const reachableBy: Map<string, Set<string>> = new Map();
for (const g of given) {
const seen = new Set<string>();
collectReachable(g, adjacencyFwd, seen);
reachableBy.set(g, seen);
}
// Keep only those not reachable from any other given root (excluding self)
const keep: string[] = [];
for (const g of given) {
let isRedundant = false;
for (const h of given) {
if (h === g) continue;
const seen = reachableBy.get(h)!;
if (seen.has(g)) {
isRedundant = true;
break;
}
}
if (!isRedundant) keep.push(g);
}
// Print one per line for easy scripting
for (const k of keep.sort()) console.log(k);
} else if (format === "dot") {
console.log(printDotForRoots(adjacency, pkgs));
} else if (format === "tree") {
// Treat multiple packages as depth-0 siblings
printTreeForest(adjacency, pkgs, depth);
} else {
console.error("Unknown format: " + format);
Deno.exit(2);
}
} catch (e) {
console.error("Error:", e);
Deno.exit(1);
}
}
if (import.meta.main) {
main();
}
// Collect all nodes reachable from start (including start)
function collectReachable(
start: string,
adjacency: Map<string, string[]>,
seen: Set<string>,
) {
if (seen.has(start)) return;
seen.add(start);
const deps = adjacency.get(start) ?? [];
for (const d of deps) collectReachable(d, adjacency, seen);
}
code:parse_tlpdb.ts
// A practical parser for texlive.tlpdb entries.
// Heuristics:
// - Blocks are separated by one or more blank lines.
// - Lines starting with whitespace are continuations of the previous key.
// - Key is first token on a line, rest is the value.
// - Dependency-related keys start with "depend" (covers "depend", "depends", etc).
// - We extract package names from dependency values by tokenizing and keeping tokens that look like package names.
export type TlpdbEntry = {
name: string;
fields: Map<string, string>;
depends: string[];
};
const NAME_KEY = "name";
export function parseTlpdb(text: string): Map<string, TlpdbEntry> {
const entries = new Map<string, TlpdbEntry>();
// Normalize newlines
const norm = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
// Split into blocks by two or more newlines (one blank line)
const blocks = norm.split(/\n{2,}/);
for (const block of blocks) {
const lines = block.split("\n").filter((l) => l.trim().length > 0);
if (lines.length === 0) continue;
const fields = new Map<string, string>();
const dependsSet = new Set<string>();
let lastKey: string | null = null;
for (const rawLine of lines) {
if (/^\s/.test(rawLine) && lastKey) {
// continuation line
const prev = fields.get(lastKey) ?? "";
fields.set(lastKey, prev + "\n" + rawLine.trim());
} else {
const m = rawLine.match(/^(\S+)\s+(.*)$/);
if (m) {
const key = m1.toLowerCase(); // Handle depend keys specially to collect all values
if (key.startsWith("depend")) {
// Extract package names from dependency value immediately
const tokens = value.split(/\s+/);
for (const t of tokens) {
const token = t.replace(/^[\(\[]+|\)\,;]+$/g, ""); if (token === "" || /^<>=!~+$/.test(token)) continue; if (/<>!=/.test(token)) continue; if (token.includes("=")) continue;
dependsSet.add(token);
}
}
}
// For all keys: if key exists, concatenate with newline; otherwise set
const prev = fields.get(key);
if (prev !== undefined) {
fields.set(key, prev + "\n" + value);
} else {
fields.set(key, value);
}
lastKey = key;
} else {
// Line with only a key?
const k = rawLine.trim().toLowerCase();
fields.set(k, "");
lastKey = k;
}
}
}
const name = fields.get(NAME_KEY) ?? null;
if (!name) continue; // skip blocks without name
const entry: TlpdbEntry = {
name,
fields,
depends: Array.from(dependsSet),
};
entries.set(name, entry);
}
return entries;
}
code:graph.ts
// Utilities to print dependency graph as tree or dot.
export function printTreeForRoots(
adjacency: Map<string, string[]>,
root: string,
maxDepth = 10,
) {
const pathVisited = new Set<string>();
console.log(root);
dfsPrint(root, adjacency, pathVisited, 1, maxDepth);
console.log("");
}
// Print a forest: treat multiple roots as depth-0 siblings in one combined view.
export function printTreeForest(
adjacency: Map<string, string[]>,
roots: string[],
maxDepth = 10,
) {
// Deduplicate roots preserving the first occurrence order
const seenRoots = new Set<string>();
const orderedRoots: string[] = [];
for (const r of roots) {
if (!seenRoots.has(r)) {
seenRoots.add(r);
orderedRoots.push(r);
}
}
for (let i = 0; i < orderedRoots.length; i++) {
const pathVisited = new Set<string>();
console.log(r);
dfsPrint(r, adjacency, pathVisited, 1, maxDepth);
if (i !== orderedRoots.length - 1) console.log("");
}
}
function dfsPrint(
node: string,
adjacency: Map<string, string[]>,
pathVisited: Set<string>,
depth: number,
maxDepth: number,
) {
if (maxDepth !== 0 && depth > maxDepth) {
console.log(" ".repeat(depth - 1) + "…");
return;
}
const deps = adjacency.get(node) ?? [];
for (const d of deps.sort()) {
const pref = " ".repeat(depth - 1) + "- ";
if (pathVisited.has(d)) {
console.log(pref + d + " (cycle)");
continue;
}
console.log(pref + d);
pathVisited.add(d);
dfsPrint(d, adjacency, pathVisited, depth + 1, maxDepth);
pathVisited.delete(d);
}
}
export function printDotForRoots(
adjacency: Map<string, string[]>,
roots: string[],
) {
// Gather reachable nodes from roots
const seen = new Set<string>();
const edges = new Set<string>();
for (const r of roots) {
collectEdges(r, adjacency, seen, edges);
}
// Emit DOT
const lines = [];
lines.push("digraph tlpdb {");
lines.push(" rankdir=LR;");
// Emit nodes (ensure deterministic order)
const nodes = Array.from(seen).sort();
for (const n of nodes) {
const attrs = [];
if (roots.includes(n)) {
attrs.push("color=red", "style=filled", 'fillcolor="#ffecec"');
}
lines.push(
` "${escapeDot(n)}" [label="${escapeDot(n)}"${
attrs.length ? "," + attrs.join(",") : ""
}];`,
);
}
// Emit edges
const edgesArr = Array.from(edges).sort();
for (const e of edgesArr) {
lines.push(" " + e + ";");
}
lines.push("}");
return lines.join("\n");
}
function collectEdges(
node: string,
adjacency: Map<string, string[]>,
seen: Set<string>,
edges: Set<string>,
) {
if (seen.has(node)) return;
seen.add(node);
const deps = adjacency.get(node) ?? [];
for (const d of deps) {
edges.add("${escapeDot(node)}" -> "${escapeDot(d)}");
collectEdges(d, adjacency, seen, edges);
}
}
function escapeDot(s: string) {
return s.replace(/"/g, '\\"');
}
インターネット経由
code:sh
for char in {A..Z}; do
echo "Packages starting with letter $char:"
echo "$package_names"
done
参考