Skip to content

Commit fec15aa

Browse files
committed
Generate table of contents for litani(7)
The litani(7) man page now contains a table of contents listing all of the other man pages, separated by chapter. This eliminates the need to manually maintain this file and keep it up-to-date with new commands. This commit fixes awslabs#87.
1 parent 3188457 commit fec15aa

File tree

3 files changed

+171
-45
lines changed

3 files changed

+171
-45
lines changed

doc/bin/gen-litani7

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License").
6+
# You may not use this file except in compliance with the License.
7+
# A copy of the License is located at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# or in the "license" file accompanying this file. This file is distributed
12+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13+
# express or implied. See the License for the specific language governing
14+
# permissions and limitations under the License.
15+
16+
17+
import argparse
18+
import pathlib
19+
import re
20+
21+
import jinja2
22+
23+
24+
_CHAPTER_DESCRIPTIONS = {
25+
"1": "Executable Commands",
26+
"5": "File Formats & Conventions",
27+
"7": "Miscellaneous",
28+
}
29+
30+
31+
def _get_args():
32+
pars = argparse.ArgumentParser()
33+
for arg in [{
34+
"flags": ["--src"],
35+
"required": True,
36+
"type": pathlib.Path,
37+
}, {
38+
"flags": ["--dst"],
39+
"required": True,
40+
"type": pathlib.Path,
41+
}, {
42+
"flags": ["--man-dir"],
43+
"required": True,
44+
"type": pathlib.Path,
45+
}]:
46+
flags = arg.pop("flags")
47+
pars.add_argument(*flags, **arg)
48+
return pars.parse_args()
49+
50+
51+
def get_metadata(man):
52+
pat = re.compile(r"^litani (?P<name>[^\s]+)\s+\-\s+(?P<description>.+)")
53+
with open(man) as handle:
54+
for line in handle:
55+
m = pat.match(line)
56+
if m:
57+
d = m["description"]
58+
description = d[0].lower() + d[1:]
59+
60+
# scdoc inserts these to avoid line breaks
61+
name = re.sub(r"\&", "", m["name"])
62+
return {
63+
"name": name,
64+
"description": description,
65+
}
66+
raise UserWarning(
67+
f"Could not extract name and description from manual {man}")
68+
69+
70+
def _add_mans(man_dir, mans_by_chapter):
71+
for man in man_dir.iterdir():
72+
if man.name == "litani.7":
73+
continue
74+
chapter = man.suffix[-1]
75+
mans_by_chapter[chapter].append(get_metadata(man))
76+
for _, mans in mans_by_chapter.items():
77+
mans.sort(key=(lambda m: m["name"]))
78+
79+
80+
def main():
81+
args = _get_args()
82+
mans_by_chapter = {chapter: [] for chapter in _CHAPTER_DESCRIPTIONS}
83+
84+
_add_mans(args.man_dir, mans_by_chapter)
85+
86+
chapters = []
87+
for num, mans in mans_by_chapter.items():
88+
chapters.append({
89+
"name": f"Chapter {num}: {_CHAPTER_DESCRIPTIONS[num]}",
90+
"mans": mans
91+
})
92+
chapters.sort(key=(lambda c: c["name"]))
93+
94+
env = jinja2.Environment(
95+
loader=jinja2.FileSystemLoader(str(args.src.parent)),
96+
autoescape=jinja2.select_autoescape(
97+
enabled_extensions=("html"),
98+
default_for_string=True))
99+
templ = env.get_template(args.src.name)
100+
page = templ.render(chapters=chapters)
101+
102+
with open(args.dst, "w") as handle:
103+
print(page, file=handle)
104+
105+
106+
if __name__ == "__main__":
107+
main()

doc/configure

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ RULES = [{
5454
" --template"
5555
f" { TEMPLATE_DIR / 'voluptuous-man.jinja.scdoc' }"
5656
" | scdoc > ${out}"
57+
}, {
58+
"name": "gen-litani7",
59+
"description": "generating litani(7).scdoc",
60+
"command":
61+
"${script}"
62+
" --dst ${out}"
63+
" --src ${src}"
64+
f" --man-dir {MAN_DIR}"
5765
}, {
5866
"name": "man_to_html",
5967
"description": "converting ${man-name} HTML",
@@ -81,28 +89,28 @@ RULES = [{
8189
}]
8290

8391

84-
def make_html_unique(man, html_man, html_mans, builds):
85-
html_unique = HTML_UNIQUE_DIR / f"{man.stem}.html"
92+
def make_html_unique(man_name, html_man, html_mans, builds):
93+
html_unique = HTML_UNIQUE_DIR / f"{man_name}.html"
8694
builds.append({
8795
"inputs": [html_man, BIN_DIR / "uniquify-header-ids"],
8896
"outputs": [html_unique],
8997
"rule": "uniquify_header_ids",
9098
"variables": {
91-
"man-name": man.stem,
99+
"man-name": man_name,
92100
"in-file": html_man,
93101
}
94102
})
95103
html_mans.append(html_unique)
96104

97105

98-
def man_to_html(man, man_out, builds):
99-
html_man = HTML_MAN_SRC_DIR / f"{man.stem}.html"
106+
def man_to_html(man_name, man_out, builds):
107+
html_man = HTML_MAN_SRC_DIR / f"{man_name}.html"
100108
builds.append({
101109
"inputs": [man_out],
102110
"outputs": [html_man],
103111
"rule": "man_to_html",
104112
"variables": {
105-
"man-name": man.stem,
113+
"man-name": man_name,
106114
"in-file": html_man,
107115
}
108116
})
@@ -112,13 +120,14 @@ def man_to_html(man, man_out, builds):
112120
def convert_man_dir_to_man(
113121
src_dir, dst_dir, rule, html_mans, builds, extra_inputs=None):
114122
for man in (src_dir).iterdir():
123+
man_name = str(man.name).split(".", 1)[0]
115124
if man.suffix == ".scdoc":
116125
with open(man) as fp:
117126
line = fp.readline().rstrip()
118127
index = line[line.find('(')+1:line.find(')')]
119-
man_out = dst_dir / f"{man.stem}.{index}"
128+
man_out = dst_dir / f"{man_name}.{index}"
120129
else:
121-
man_out = dst_dir / f"{man.stem}.5"
130+
man_out = dst_dir / f"{man_name}.5"
122131
inputs = [man]
123132
if extra_inputs:
124133
inputs.extend(extra_inputs)
@@ -127,12 +136,43 @@ def convert_man_dir_to_man(
127136
"outputs": [man_out],
128137
"rule": rule,
129138
"variables": {
130-
"man-name": man.stem,
139+
"man-name": man_name,
131140
"data-path": man.resolve(),
132141
}
133142
})
134-
html_man = man_to_html(man, man_out, builds)
135-
make_html_unique(man, html_man, html_mans, builds)
143+
html_man = man_to_html(man_name, man_out, builds)
144+
make_html_unique(man_name, html_man, html_mans, builds)
145+
146+
147+
def add_litani_7(builds, html_mans):
148+
"""The litani(7) man page is special because it contains a table of contents
149+
of the other pages, so it depends on the others."""
150+
151+
templ = SRC_DIR / "litani7" / "litani.jinja.scdoc"
152+
script = BIN_DIR / "gen-litani7"
153+
154+
inputs = [b["outputs"][0] for b in builds if str(b["outputs"][0])[-1].isdigit()]
155+
inputs.extend([templ, script])
156+
157+
sc_out = TMP_DIR / "litani.scdoc"
158+
man_out = TMP_DIR / "litani.7"
159+
160+
builds.extend([{
161+
"inputs": inputs,
162+
"outputs": [sc_out],
163+
"rule": "gen-litani7",
164+
"variables": {
165+
"src": templ,
166+
"script": script,
167+
"man-dir": MAN_DIR,
168+
}
169+
}, {
170+
"inputs": [sc_out],
171+
"outputs": [man_out],
172+
"rule": "sc_to_man",
173+
}])
174+
html_man = man_to_html("litani.7", man_out, builds)
175+
make_html_unique("litani.7", html_man, html_mans, builds)
136176

137177

138178
def main():
@@ -145,6 +185,8 @@ def main():
145185
SRC_DIR / "voluptuous-man", MAN_DIR, "voluptuous_to_man", html_mans,
146186
builds, extra_inputs=[TEMPLATE_DIR / "voluptuous-man.jinja.scdoc"])
147187

188+
add_litani_7(builds, html_mans)
189+
148190
builds.append({
149191
"inputs": html_mans + [
150192
BIN_DIR / "build-html-doc",
@@ -162,6 +204,8 @@ def main():
162204
for k, v in build.items():
163205
if isinstance(v, list):
164206
build[k] = [str(s) for s in v]
207+
elif isinstance(v, dict):
208+
build[k] = {ik: str(iv) for ik, iv in v.items()}
165209
try:
166210
build["implicit"].append(str(DOC_DIR / "configure"))
167211
except KeyError:

doc/src/litani7/litani.jinja.scdoc

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,44 +55,19 @@ running, and use the _run.json_ file during or after the build to gain
5555
insight into the build outcome.
5656

5757

58-
# COMMAND LINE INTERFACE
58+
# MANUAL PAGES -- TABLE OF CONTENTS
5959

60-
The following is a synopsis of Litani's subcommands:
60+
The following man pages are available with local installations of Litani, or
61+
online at _https://awslabs.github.io/aws-build-accumulator/_:
6162

62-
*litani init*
63-
Create a new Litani build, allowing you to add jobs and subsequently
64-
run them.
63+
{% for chapter in chapters %}{% if chapter["mans"] %}
6564

66-
*litani add-job*
67-
Add a job to the build. A 'job' is a command that will be executed as
68-
part of the build, possibly with inputs and outputs that describe its
69-
place in the dependency graph, and other attributes.
70-
71-
*litani run-build*
72-
Execute all jobs that have been added to the build since the last time
73-
you ran *litani init*.
74-
75-
*litani dump-run*
76-
Print a JSON representation of the run so far to stdout both while the
77-
build is running as well as after it has completed.
78-
79-
*litani print-html-dir*
80-
Print the path to a continually-updated HTML report directory
81-
82-
*litani acquire-html-dir*
83-
Print the path to a locked HTML report directory
84-
85-
*litani release-html-dir*
86-
Unlock a previously-locked HTML report directory
87-
88-
*litani graph*
89-
Print the dependency graph of the build in Graphviz format, ready to
90-
be rendered into an image using *dot(1)*.
91-
92-
*litani print-capabilities*
93-
Print a list of features that this version of Litani supports. This
94-
can be used as an alternative to checking the version string.
65+
## {{ chapter["name"] }}
9566

67+
{% for man in chapter["mans"] %}
68+
*litani {{ man["name"] }}*: {{ man["description"] }}{% if not loop.last %}++{% endif %}
69+
{%- endfor -%}
70+
{% endif %}{% endfor %}
9671

9772
# SEE ALSO
9873

0 commit comments

Comments
 (0)