From 3121c10c737a30d1288ad3955f9105dcb193df73 Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Mon, 6 Nov 2023 12:20:00 +0000 Subject: Remove language file extension from bin/* Putting ".sh" or ".py" on the end of scripts is an antipattern. It makes it inconvenient to change the implementation language. Change all call sites. --- bin/build_html | 17 ++++ bin/build_html.sh | 17 ---- bin/check_index | 2 +- bin/make_redirects | 78 ++++++++++++++++++ bin/make_redirects.py | 78 ------------------ bin/reindex | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++ bin/reindex.py | 215 -------------------------------------------------- 7 files changed, 311 insertions(+), 311 deletions(-) create mode 100755 bin/build_html delete mode 100755 bin/build_html.sh create mode 100755 bin/make_redirects delete mode 100755 bin/make_redirects.py create mode 100755 bin/reindex delete mode 100755 bin/reindex.py (limited to 'bin') diff --git a/bin/build_html b/bin/build_html new file mode 100755 index 0000000..026c525 --- /dev/null +++ b/bin/build_html @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail -x + +: ${MDBOOK:=mdbook} + +TOPLEVEL=$(realpath $(dirname "$0"))/.. +cd "${TOPLEVEL}" +./bin/reindex + +./bin/make_redirects + +cd "${TOPLEVEL}/mdbook/spec" +$MDBOOK build + +cd "${TOPLEVEL}/mdbook/proposals" +$MDBOOK build diff --git a/bin/build_html.sh b/bin/build_html.sh deleted file mode 100755 index 09b92e2..0000000 --- a/bin/build_html.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -e -u -o pipefail -x - -: ${MDBOOK:=mdbook} - -TOPLEVEL=$(realpath $(dirname "$0"))/.. -cd "${TOPLEVEL}" -./bin/reindex.py - -./bin/make_redirects.py - -cd "${TOPLEVEL}/mdbook/spec" -$MDBOOK build - -cd "${TOPLEVEL}/mdbook/proposals" -$MDBOOK build diff --git a/bin/check_index b/bin/check_index index 77bdb02..a2e8c4f 100755 --- a/bin/check_index +++ b/bin/check_index @@ -14,7 +14,7 @@ if ! git diff --quiet ; then fi cd "$TOPLEVEL/" -./bin/reindex.py +./bin/reindex if ! git diff --quiet ; then echo "Proposal index is not up-to-date. Run ./reindex.py to regenerate it." >&2 diff --git a/bin/make_redirects b/bin/make_redirects new file mode 100755 index 0000000..ea8ea3b --- /dev/null +++ b/bin/make_redirects @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import yaml, os, re, sys + +def update_file(fname, start_marker, end_marker, replacement): + content = list(open(fname, 'r').readlines()) + startline = content.index(start_marker) + endline = content.index(end_marker) + assert endline > startline + content[startline+1:endline] = replacement + + with open(fname+".tmp", 'w') as f: + f.write("".join(content)) + os.rename(fname+".tmp", fname) + +BOOK_START = "# BEGIN AUTO-GENERATED REDIRECTS\n" +BOOK_END = "# END AUTO-GENERATED REDIRECTS\n" + +README_START = "\n" +README_END = "\n" + +def book_redirects(rs, spec_dir): + lines = [] + for kwd, info in rs.items(): + if os.path.isdir(os.path.join(spec_dir, kwd)): + assert os.path.isfile(os.path.join(spec_dir, kwd, "index.md")) + assert info.get('implicit') + continue + elif any((os.path.isfile(os.path.join(spec_dir, kwd) + ext)) for ext in [".txt", ".md"]): + assert info.get('implicit') + continue + else: + assert not info.get('implicit') + source = kwd + ".html" + target = info['target'] + lines.append( + f'"/{source}" = "{target}"\n' + ) + return "".join(lines) + +def readme_redirects(rs): + lines = [ "
\n" ] + for kwd, info in rs.items(): + target = info['target'] + desc = info['description'] + lines.append(f'
/{kwd}
\n') + lines.append(f'
{target} ({desc})\n') + + lines.append("
\n") + return "".join(lines) + +def proposal_redirects(proposals_dir): + lines = [] + for fname in os.listdir(proposals_dir): + m = re.match(r'^(\d+)-.*\.(?:md|txt)$', fname) + if m: + source = m.group(1) + ".html" + target, targetext = os.path.splitext(fname) + if targetext == '.md': + targetext = ".html" + lines.append(f'"/{source}" = "./{target}{targetext}"\n') + lines.sort() + return "".join(lines) + +if __name__ == '__main__': + toplevel = os.path.join(os.path.dirname(sys.argv[0]), "..") + spec_book_fname = os.path.join(toplevel, "mdbook", "spec", "book.toml") + spec_dir = os.path.join(toplevel, "spec") + readme_fname = os.path.join(toplevel, "spec", "README.md") + prop_dir = os.path.join(toplevel, "proposals") + prop_book_fname = os.path.join(toplevel, "mdbook", "proposals", "book.toml") + yaml_fname = os.path.join(toplevel, "mdbook", "spec", "spec-redirects.yaml") + + rs = yaml.load(open(yaml_fname), yaml.Loader)['redirects'] + + update_file(spec_book_fname, BOOK_START, BOOK_END, book_redirects(rs, spec_dir)) + update_file(readme_fname, README_START, README_END, readme_redirects(rs)) + update_file(prop_book_fname, BOOK_START, BOOK_END, proposal_redirects(prop_dir)) diff --git a/bin/make_redirects.py b/bin/make_redirects.py deleted file mode 100755 index ea8ea3b..0000000 --- a/bin/make_redirects.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 - -import yaml, os, re, sys - -def update_file(fname, start_marker, end_marker, replacement): - content = list(open(fname, 'r').readlines()) - startline = content.index(start_marker) - endline = content.index(end_marker) - assert endline > startline - content[startline+1:endline] = replacement - - with open(fname+".tmp", 'w') as f: - f.write("".join(content)) - os.rename(fname+".tmp", fname) - -BOOK_START = "# BEGIN AUTO-GENERATED REDIRECTS\n" -BOOK_END = "# END AUTO-GENERATED REDIRECTS\n" - -README_START = "\n" -README_END = "\n" - -def book_redirects(rs, spec_dir): - lines = [] - for kwd, info in rs.items(): - if os.path.isdir(os.path.join(spec_dir, kwd)): - assert os.path.isfile(os.path.join(spec_dir, kwd, "index.md")) - assert info.get('implicit') - continue - elif any((os.path.isfile(os.path.join(spec_dir, kwd) + ext)) for ext in [".txt", ".md"]): - assert info.get('implicit') - continue - else: - assert not info.get('implicit') - source = kwd + ".html" - target = info['target'] - lines.append( - f'"/{source}" = "{target}"\n' - ) - return "".join(lines) - -def readme_redirects(rs): - lines = [ "
\n" ] - for kwd, info in rs.items(): - target = info['target'] - desc = info['description'] - lines.append(f'
/{kwd}
\n') - lines.append(f'
{target} ({desc})\n') - - lines.append("
\n") - return "".join(lines) - -def proposal_redirects(proposals_dir): - lines = [] - for fname in os.listdir(proposals_dir): - m = re.match(r'^(\d+)-.*\.(?:md|txt)$', fname) - if m: - source = m.group(1) + ".html" - target, targetext = os.path.splitext(fname) - if targetext == '.md': - targetext = ".html" - lines.append(f'"/{source}" = "./{target}{targetext}"\n') - lines.sort() - return "".join(lines) - -if __name__ == '__main__': - toplevel = os.path.join(os.path.dirname(sys.argv[0]), "..") - spec_book_fname = os.path.join(toplevel, "mdbook", "spec", "book.toml") - spec_dir = os.path.join(toplevel, "spec") - readme_fname = os.path.join(toplevel, "spec", "README.md") - prop_dir = os.path.join(toplevel, "proposals") - prop_book_fname = os.path.join(toplevel, "mdbook", "proposals", "book.toml") - yaml_fname = os.path.join(toplevel, "mdbook", "spec", "spec-redirects.yaml") - - rs = yaml.load(open(yaml_fname), yaml.Loader)['redirects'] - - update_file(spec_book_fname, BOOK_START, BOOK_END, book_redirects(rs, spec_dir)) - update_file(readme_fname, README_START, README_END, readme_redirects(rs)) - update_file(prop_book_fname, BOOK_START, BOOK_END, proposal_redirects(prop_dir)) diff --git a/bin/reindex b/bin/reindex new file mode 100755 index 0000000..57ac403 --- /dev/null +++ b/bin/reindex @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +import sys +if sys.version_info[0] < 3: + print("No support for Python 2.") + sys.exit(1) + +import codecs, re, os +class Error(Exception): pass + +STATUSES = """DRAFT NEEDS-REVISION NEEDS-RESEARCH OPEN ACCEPTED META FINISHED + CLOSED SUPERSEDED DEAD REJECTED OBSOLETE RESERVE INFORMATIONAL""".split() +REQUIRED_FIELDS = [ "Filename", "Status", "Title"] +CONDITIONAL_FIELDS = { "OPEN" : [ "Target", "Ticket" ], + "ACCEPTED" : [ "Target", "Ticket" ], + "CLOSED" : [ "Implemented-In", "Ticket" ], + "FINISHED" : [ "Implemented-In", "Ticket" ] } +FNAME_RE = re.compile(r'^(\d\d\d)-.*[^\~]$') +DIR = "." +OUTFILE_TXT = "000-index.txt" +TMPFILE_TXT = OUTFILE_TXT+".tmp" + +TEMPFILES = [TMPFILE_TXT] + +def unlink_if_present(fname): + try: + os.unlink(fname) + except OSError: + pass + +def indexed(seq): + n = 0 + for i in seq: + yield n, i + n += 1 + +def readProposal(fn): + fields = { } + f = codecs.open(fn, 'r', encoding='utf-8') + lastField = None + try: + for lineno, line in indexed(f): + line = line.rstrip() + if not line: + return fields + if line[0].isspace(): + fields[lastField] += " %s"%(line.strip()) + elif line == "```": + pass + else: + parts = line.split(":", 1) + if len(parts) != 2: + raise Error("%s:%s: Neither field, continuation, nor ```."% + (fn,lineno)) + else: + fields[parts[0]] = parts[1].strip() + lastField = parts[0] + + return fields + finally: + f.close() + +def getProposalNumber(fn): + """Get the proposal's assigned number from its filename `fn`.""" + parts = fn.split('-', 1) + + assert len(parts) == 2, \ + "Filename must have a proposal number and title separated by a '-'" + + return int(parts[0]) + +def checkProposal(fn, fields): + status = fields.get("Status") + need_fields = REQUIRED_FIELDS + CONDITIONAL_FIELDS.get(status, []) + + number = getProposalNumber(fn) + # Since prop#288 was the newest when we began requiring the 'Ticket:' + # field, we don't require the field for it or any older proposal. + # (Although you're encouraged to add it to your proposal, and add it for + # older proposals where you know the correct ticket, as it greatly helps + # newcomers find more information on the implementation.) + if number <= 288: + if "Ticket" in need_fields: + need_fields.remove("Ticket") + + for f in need_fields: + if f not in fields: + raise Error("%s has no %s field"%(fn, f)) + if fn != fields['Filename']: + raise Error("Mismatched Filename field in %s"%fn) + if fields['Title'][-1] == '.': + fields['Title'] = fields['Title'][:-1] + + status = fields['Status'] = status.upper() + if status not in STATUSES: + raise Error("I've never heard of status %s in %s"%(status,fn)) + if status in [ "SUPERSEDED", "DEAD" ]: + for f in [ 'Implemented-In', 'Target' ]: + if f in fields: del fields[f] + + fields['FilenameTruncated'] = os.path.splitext(fields['Filename'])[0] + +def readProposals(): + res = [] + for fn in os.listdir(DIR): + m = FNAME_RE.match(fn) + if not m: continue + if fn.endswith(".tmp"): + continue + if not (fn.endswith(".txt") or fn.endswith(".md")): + raise Error("%s doesn't end with .txt or .md"%fn) + num = m.group(1) + fields = readProposal(fn) + checkProposal(fn, fields) + fields['num'] = num + res.append(fields) + return res + +def writeTextIndexFile(proposals): + proposals.sort(key=lambda f:f['num']) + seenStatuses = set() + for p in proposals: + seenStatuses.add(p['Status']) + + out = open(TMPFILE_TXT, 'w') + inf = open(OUTFILE_TXT, 'r') + for line in inf: + out.write(line) + if line.startswith("====="): break + inf.close() + + out.write("Proposals by number:\n\n") + for prop in proposals: + out.write("%(num)s %(Title)s [%(Status)s]\n"%prop) + out.write("\n\nProposals by status:\n\n") + for s in STATUSES: + if s not in seenStatuses: continue + out.write(" %s:\n"%s) + for prop in proposals: + if s == prop['Status']: + out.write(" %(num)s %(Title)s"%prop) + if "Target" in prop: + out.write(" [for %(Target)s]"%prop) + if "Implemented-In" in prop: + out.write(" [in %(Implemented-In)s]"%prop) + out.write("\n") + + out.write("```\n") + out.close() + os.rename(TMPFILE_TXT, OUTFILE_TXT) + +def formatMarkdownEntry(prop, withStatus=False): + if withStatus: + fmt = "* [`{Filename}`](/proposals/{Filename}): {Title} [{Status}]\n" + else: + fmt = "* [`{Filename}`](/proposals/{Filename}): {Title}\n" + return fmt.format(**prop) + +def formatSummaryEntry(prop): + return " - [`{FilenameTruncated}`](./{Filename}): {Title} ({Status})\n".format(**prop) + +def writeMarkdownFile(prefix, format_inputs): + template = prefix+"_template.md" + output = prefix+".md" + t = open(template).read() + content = t.format(**format_inputs) + with open(output, 'w') as f: + f.write(content) + +def writeMarkdownIndexFiles(proposals): + markdown_files = [ "BY_INDEX", "BY_STATUS", "SUMMARY" ] + format_inputs = {} + + format_inputs['warning'] = "" + + entries = [] + for prop in proposals: + entries.append(formatMarkdownEntry(prop, withStatus=True)) + format_inputs["BY_INDEX"] = "".join(entries) + + entries = [] + for prop in proposals: + entries.append(formatSummaryEntry(prop)) + format_inputs["SUMMARY_TABLE"] = "".join(entries) + + for s in STATUSES: + entries = [] + for prop in proposals: + if s == prop['Status']: + entries.append(formatMarkdownEntry(prop)) + if entries: + format_inputs[s] = "".join(entries) + else: + format_inputs[s] = "(There are no proposals in this category)\n" + + entries = [] + for prop in proposals: + if prop['Status'] in ('DEAD', 'REJECTED', 'OBSOLETE'): + entries.append(formatMarkdownEntry(prop, withStatus=True)) + format_inputs['DEAD_REJECTED_OBSOLETE'] = "".join(entries) + + for prefix in markdown_files: + writeMarkdownFile(prefix, format_inputs) + +if __name__ == '__main__': + proposal_dir = os.path.join(os.path.dirname(sys.argv[0]), "..", "proposals") + os.chdir(proposal_dir) + + proposals = readProposals() + try: + writeTextIndexFile(proposals) + writeMarkdownIndexFiles(proposals) + finally: + for tempfile in TEMPFILES: + unlink_if_present(tempfile) diff --git a/bin/reindex.py b/bin/reindex.py deleted file mode 100755 index 57ac403..0000000 --- a/bin/reindex.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 - -import sys -if sys.version_info[0] < 3: - print("No support for Python 2.") - sys.exit(1) - -import codecs, re, os -class Error(Exception): pass - -STATUSES = """DRAFT NEEDS-REVISION NEEDS-RESEARCH OPEN ACCEPTED META FINISHED - CLOSED SUPERSEDED DEAD REJECTED OBSOLETE RESERVE INFORMATIONAL""".split() -REQUIRED_FIELDS = [ "Filename", "Status", "Title"] -CONDITIONAL_FIELDS = { "OPEN" : [ "Target", "Ticket" ], - "ACCEPTED" : [ "Target", "Ticket" ], - "CLOSED" : [ "Implemented-In", "Ticket" ], - "FINISHED" : [ "Implemented-In", "Ticket" ] } -FNAME_RE = re.compile(r'^(\d\d\d)-.*[^\~]$') -DIR = "." -OUTFILE_TXT = "000-index.txt" -TMPFILE_TXT = OUTFILE_TXT+".tmp" - -TEMPFILES = [TMPFILE_TXT] - -def unlink_if_present(fname): - try: - os.unlink(fname) - except OSError: - pass - -def indexed(seq): - n = 0 - for i in seq: - yield n, i - n += 1 - -def readProposal(fn): - fields = { } - f = codecs.open(fn, 'r', encoding='utf-8') - lastField = None - try: - for lineno, line in indexed(f): - line = line.rstrip() - if not line: - return fields - if line[0].isspace(): - fields[lastField] += " %s"%(line.strip()) - elif line == "```": - pass - else: - parts = line.split(":", 1) - if len(parts) != 2: - raise Error("%s:%s: Neither field, continuation, nor ```."% - (fn,lineno)) - else: - fields[parts[0]] = parts[1].strip() - lastField = parts[0] - - return fields - finally: - f.close() - -def getProposalNumber(fn): - """Get the proposal's assigned number from its filename `fn`.""" - parts = fn.split('-', 1) - - assert len(parts) == 2, \ - "Filename must have a proposal number and title separated by a '-'" - - return int(parts[0]) - -def checkProposal(fn, fields): - status = fields.get("Status") - need_fields = REQUIRED_FIELDS + CONDITIONAL_FIELDS.get(status, []) - - number = getProposalNumber(fn) - # Since prop#288 was the newest when we began requiring the 'Ticket:' - # field, we don't require the field for it or any older proposal. - # (Although you're encouraged to add it to your proposal, and add it for - # older proposals where you know the correct ticket, as it greatly helps - # newcomers find more information on the implementation.) - if number <= 288: - if "Ticket" in need_fields: - need_fields.remove("Ticket") - - for f in need_fields: - if f not in fields: - raise Error("%s has no %s field"%(fn, f)) - if fn != fields['Filename']: - raise Error("Mismatched Filename field in %s"%fn) - if fields['Title'][-1] == '.': - fields['Title'] = fields['Title'][:-1] - - status = fields['Status'] = status.upper() - if status not in STATUSES: - raise Error("I've never heard of status %s in %s"%(status,fn)) - if status in [ "SUPERSEDED", "DEAD" ]: - for f in [ 'Implemented-In', 'Target' ]: - if f in fields: del fields[f] - - fields['FilenameTruncated'] = os.path.splitext(fields['Filename'])[0] - -def readProposals(): - res = [] - for fn in os.listdir(DIR): - m = FNAME_RE.match(fn) - if not m: continue - if fn.endswith(".tmp"): - continue - if not (fn.endswith(".txt") or fn.endswith(".md")): - raise Error("%s doesn't end with .txt or .md"%fn) - num = m.group(1) - fields = readProposal(fn) - checkProposal(fn, fields) - fields['num'] = num - res.append(fields) - return res - -def writeTextIndexFile(proposals): - proposals.sort(key=lambda f:f['num']) - seenStatuses = set() - for p in proposals: - seenStatuses.add(p['Status']) - - out = open(TMPFILE_TXT, 'w') - inf = open(OUTFILE_TXT, 'r') - for line in inf: - out.write(line) - if line.startswith("====="): break - inf.close() - - out.write("Proposals by number:\n\n") - for prop in proposals: - out.write("%(num)s %(Title)s [%(Status)s]\n"%prop) - out.write("\n\nProposals by status:\n\n") - for s in STATUSES: - if s not in seenStatuses: continue - out.write(" %s:\n"%s) - for prop in proposals: - if s == prop['Status']: - out.write(" %(num)s %(Title)s"%prop) - if "Target" in prop: - out.write(" [for %(Target)s]"%prop) - if "Implemented-In" in prop: - out.write(" [in %(Implemented-In)s]"%prop) - out.write("\n") - - out.write("```\n") - out.close() - os.rename(TMPFILE_TXT, OUTFILE_TXT) - -def formatMarkdownEntry(prop, withStatus=False): - if withStatus: - fmt = "* [`{Filename}`](/proposals/{Filename}): {Title} [{Status}]\n" - else: - fmt = "* [`{Filename}`](/proposals/{Filename}): {Title}\n" - return fmt.format(**prop) - -def formatSummaryEntry(prop): - return " - [`{FilenameTruncated}`](./{Filename}): {Title} ({Status})\n".format(**prop) - -def writeMarkdownFile(prefix, format_inputs): - template = prefix+"_template.md" - output = prefix+".md" - t = open(template).read() - content = t.format(**format_inputs) - with open(output, 'w') as f: - f.write(content) - -def writeMarkdownIndexFiles(proposals): - markdown_files = [ "BY_INDEX", "BY_STATUS", "SUMMARY" ] - format_inputs = {} - - format_inputs['warning'] = "" - - entries = [] - for prop in proposals: - entries.append(formatMarkdownEntry(prop, withStatus=True)) - format_inputs["BY_INDEX"] = "".join(entries) - - entries = [] - for prop in proposals: - entries.append(formatSummaryEntry(prop)) - format_inputs["SUMMARY_TABLE"] = "".join(entries) - - for s in STATUSES: - entries = [] - for prop in proposals: - if s == prop['Status']: - entries.append(formatMarkdownEntry(prop)) - if entries: - format_inputs[s] = "".join(entries) - else: - format_inputs[s] = "(There are no proposals in this category)\n" - - entries = [] - for prop in proposals: - if prop['Status'] in ('DEAD', 'REJECTED', 'OBSOLETE'): - entries.append(formatMarkdownEntry(prop, withStatus=True)) - format_inputs['DEAD_REJECTED_OBSOLETE'] = "".join(entries) - - for prefix in markdown_files: - writeMarkdownFile(prefix, format_inputs) - -if __name__ == '__main__': - proposal_dir = os.path.join(os.path.dirname(sys.argv[0]), "..", "proposals") - os.chdir(proposal_dir) - - proposals = readProposals() - try: - writeTextIndexFile(proposals) - writeMarkdownIndexFiles(proposals) - finally: - for tempfile in TEMPFILES: - unlink_if_present(tempfile) -- cgit v1.2.3-54-g00ecf