#!/bin/bash # Summarizes a Makefile's targets as a markdown bullet list. # # Usage: skim-makefile PATH # # Emits a markdown fragment (a bullet list, possibly preceded by a short # prose line). No headings: the caller frames the section. # # Hard rule: parse only, never invoke. # Repos can be untrusted content; the existence of a Makefile must not # translate into code execution. We never run `make`, never expand # variables, never follow includes. A Makefile that defines targets via # include or eval will look empty here, and that's the correct safe behavior. # # Two-pass parse, in order of preference: # # 1. Annotated targets: `name: [deps] ## description`. # A widely cargo-culted convention (often paired with a `help` target # that greps for the same pattern). When present, treated as the # author's curated entrypoint surface -- unannotated targets in such a # Makefile are typically private helpers and are dropped. # # 2. Bare targets: `name:` or `name: deps`. # Fallback when no annotations are found. Filters pattern rules (`%`), # variable assignments (regex requires `:` followed by space or EOL, # which naturally excludes `:=`), and dotted internals (`.PHONY`, ...). set -euo pipefail if [ $# -lt 1 ]; then echo "usage: skim-makefile PATH" >&2 exit 2 fi makefile="$1" if [ ! -f "$makefile" ]; then echo "skim-makefile: not a file: $makefile" >&2 exit 2 fi # Cap on listed targets; large recipe lists self-truncate with a counter. MAX_TARGETS=50 annotated=$(grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*:[^=]*##' "$makefile" || true) if [ -n "$annotated" ]; then echo "Annotated targets (\`name: ## description\` convention):" echo printf '%s\n' "$annotated" \ | awk -F'##' '{ split($1, a, ":"); name = a[1]; gsub(/^[ \t]+|[ \t]+$/, "", name); desc = $2; gsub(/^[ \t]+|[ \t]+$/, "", desc); printf "- `%s` -- %s\n", name, desc; }' \ | head -n "$MAX_TARGETS" total=$(printf '%s\n' "$annotated" | wc -l) if [ "$total" -gt "$MAX_TARGETS" ]; then echo "- ... ($((total - MAX_TARGETS)) more not shown)" fi exit 0 fi # Bare-target parse. mapfile -t targets < <( grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*::?([[:space:]]|$)' "$makefile" \ | grep -v '%' \ | sed -E 's/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*).*/\1/' \ | awk '!seen[$0]++' \ || true ) if [ ${#targets[@]} -gt 0 ]; then echo "Targets (parsed; no \`## description\` annotations found):" echo n=0 for t in "${targets[@]}"; do if [ "$n" -ge "$MAX_TARGETS" ]; then echo "- ... ($((${#targets[@]} - MAX_TARGETS)) more not shown)" break fi echo "- \`$t\`" n=$((n + 1)) done exit 0 fi # File present but yielded no targets we recognize. # Useful signal in itself: tells the caller "go read it". echo "(No targets parsed; the Makefile may define targets via include or eval.)"