Loading...
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# -*- coding: utf-8; mode: python -*-
4
5"""
6 Script to auto generate the documentation for Netlink specifications.
7
8 :copyright: Copyright (C) 2023 Breno Leitao <leitao@debian.org>
9 :license: GPL Version 2, June 1991 see linux/COPYING for details.
10
11 This script performs extensive parsing to the Linux kernel's netlink YAML
12 spec files, in an effort to avoid needing to heavily mark up the original
13 YAML file.
14
15 This code is split in three big parts:
16 1) RST formatters: Use to convert a string to a RST output
17 2) Parser helpers: Functions to parse the YAML data structure
18 3) Main function and small helpers
19"""
20
21from typing import Any, Dict, List
22import os.path
23import sys
24import argparse
25import logging
26import yaml
27
28
29SPACE_PER_LEVEL = 4
30
31
32# RST Formatters
33# ==============
34def headroom(level: int) -> str:
35 """Return space to format"""
36 return " " * (level * SPACE_PER_LEVEL)
37
38
39def bold(text: str) -> str:
40 """Format bold text"""
41 return f"**{text}**"
42
43
44def inline(text: str) -> str:
45 """Format inline text"""
46 return f"``{text}``"
47
48
49def sanitize(text: str) -> str:
50 """Remove newlines and multiple spaces"""
51 # This is useful for some fields that are spread across multiple lines
52 return str(text).replace("\n", "").strip()
53
54
55def rst_fields(key: str, value: str, level: int = 0) -> str:
56 """Return a RST formatted field"""
57 return headroom(level) + f":{key}: {value}"
58
59
60def rst_definition(key: str, value: Any, level: int = 0) -> str:
61 """Format a single rst definition"""
62 return headroom(level) + key + "\n" + headroom(level + 1) + str(value)
63
64
65def rst_paragraph(paragraph: str, level: int = 0) -> str:
66 """Return a formatted paragraph"""
67 return headroom(level) + paragraph
68
69
70def rst_bullet(item: str, level: int = 0) -> str:
71 """Return a formatted a bullet"""
72 return headroom(level) + f"- {item}"
73
74
75def rst_subsection(title: str) -> str:
76 """Add a sub-section to the document"""
77 return f"{title}\n" + "-" * len(title)
78
79
80def rst_subsubsection(title: str) -> str:
81 """Add a sub-sub-section to the document"""
82 return f"{title}\n" + "~" * len(title)
83
84
85def rst_section(title: str) -> str:
86 """Add a section to the document"""
87 return f"\n{title}\n" + "=" * len(title)
88
89
90def rst_subtitle(title: str) -> str:
91 """Add a subtitle to the document"""
92 return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n"
93
94
95def rst_title(title: str) -> str:
96 """Add a title to the document"""
97 return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n"
98
99
100def rst_list_inline(list_: List[str], level: int = 0) -> str:
101 """Format a list using inlines"""
102 return headroom(level) + "[" + ", ".join(inline(i) for i in list_) + "]"
103
104
105def rst_header() -> str:
106 """The headers for all the auto generated RST files"""
107 lines = []
108
109 lines.append(rst_paragraph(".. SPDX-License-Identifier: GPL-2.0"))
110 lines.append(rst_paragraph(".. NOTE: This document was auto-generated.\n\n"))
111
112 return "\n".join(lines)
113
114
115def rst_toctree(maxdepth: int = 2) -> str:
116 """Generate a toctree RST primitive"""
117 lines = []
118
119 lines.append(".. toctree::")
120 lines.append(f" :maxdepth: {maxdepth}\n\n")
121
122 return "\n".join(lines)
123
124
125def rst_label(title: str) -> str:
126 """Return a formatted label"""
127 return f".. _{title}:\n\n"
128
129
130# Parsers
131# =======
132
133
134def parse_mcast_group(mcast_group: List[Dict[str, Any]]) -> str:
135 """Parse 'multicast' group list and return a formatted string"""
136 lines = []
137 for group in mcast_group:
138 lines.append(rst_bullet(group["name"]))
139
140 return "\n".join(lines)
141
142
143def parse_do(do_dict: Dict[str, Any], level: int = 0) -> str:
144 """Parse 'do' section and return a formatted string"""
145 lines = []
146 for key in do_dict.keys():
147 lines.append(rst_paragraph(bold(key), level + 1))
148 lines.append(parse_do_attributes(do_dict[key], level + 1) + "\n")
149
150 return "\n".join(lines)
151
152
153def parse_do_attributes(attrs: Dict[str, Any], level: int = 0) -> str:
154 """Parse 'attributes' section"""
155 if "attributes" not in attrs:
156 return ""
157 lines = [rst_fields("attributes", rst_list_inline(attrs["attributes"]), level + 1)]
158
159 return "\n".join(lines)
160
161
162def parse_operations(operations: List[Dict[str, Any]]) -> str:
163 """Parse operations block"""
164 preprocessed = ["name", "doc", "title", "do", "dump"]
165 lines = []
166
167 for operation in operations:
168 lines.append(rst_section(operation["name"]))
169 lines.append(rst_paragraph(sanitize(operation["doc"])) + "\n")
170
171 for key in operation.keys():
172 if key in preprocessed:
173 # Skip the special fields
174 continue
175 lines.append(rst_fields(key, operation[key], 0))
176
177 if "do" in operation:
178 lines.append(rst_paragraph(":do:", 0))
179 lines.append(parse_do(operation["do"], 0))
180 if "dump" in operation:
181 lines.append(rst_paragraph(":dump:", 0))
182 lines.append(parse_do(operation["dump"], 0))
183
184 # New line after fields
185 lines.append("\n")
186
187 return "\n".join(lines)
188
189
190def parse_entries(entries: List[Dict[str, Any]], level: int) -> str:
191 """Parse a list of entries"""
192 lines = []
193 for entry in entries:
194 if isinstance(entry, dict):
195 # entries could be a list or a dictionary
196 lines.append(
197 rst_fields(entry.get("name", ""), sanitize(entry.get("doc", "")), level)
198 )
199 elif isinstance(entry, list):
200 lines.append(rst_list_inline(entry, level))
201 else:
202 lines.append(rst_bullet(inline(sanitize(entry)), level))
203
204 lines.append("\n")
205 return "\n".join(lines)
206
207
208def parse_definitions(defs: Dict[str, Any]) -> str:
209 """Parse definitions section"""
210 preprocessed = ["name", "entries", "members"]
211 ignored = ["render-max"] # This is not printed
212 lines = []
213
214 for definition in defs:
215 lines.append(rst_section(definition["name"]))
216 for k in definition.keys():
217 if k in preprocessed + ignored:
218 continue
219 lines.append(rst_fields(k, sanitize(definition[k]), 0))
220
221 # Field list needs to finish with a new line
222 lines.append("\n")
223 if "entries" in definition:
224 lines.append(rst_paragraph(":entries:", 0))
225 lines.append(parse_entries(definition["entries"], 1))
226 if "members" in definition:
227 lines.append(rst_paragraph(":members:", 0))
228 lines.append(parse_entries(definition["members"], 1))
229
230 return "\n".join(lines)
231
232
233def parse_attr_sets(entries: List[Dict[str, Any]]) -> str:
234 """Parse attribute from attribute-set"""
235 preprocessed = ["name", "type"]
236 ignored = ["checks"]
237 lines = []
238
239 for entry in entries:
240 lines.append(rst_section(entry["name"]))
241 for attr in entry["attributes"]:
242 type_ = attr.get("type")
243 attr_line = attr["name"]
244 if type_:
245 # Add the attribute type in the same line
246 attr_line += f" ({inline(type_)})"
247
248 lines.append(rst_subsubsection(attr_line))
249
250 for k in attr.keys():
251 if k in preprocessed + ignored:
252 continue
253 lines.append(rst_fields(k, sanitize(attr[k]), 0))
254 lines.append("\n")
255
256 return "\n".join(lines)
257
258
259def parse_sub_messages(entries: List[Dict[str, Any]]) -> str:
260 """Parse sub-message definitions"""
261 lines = []
262
263 for entry in entries:
264 lines.append(rst_section(entry["name"]))
265 for fmt in entry["formats"]:
266 value = fmt["value"]
267
268 lines.append(rst_bullet(bold(value)))
269 for attr in ['fixed-header', 'attribute-set']:
270 if attr in fmt:
271 lines.append(rst_fields(attr, fmt[attr], 1))
272 lines.append("\n")
273
274 return "\n".join(lines)
275
276
277def parse_yaml(obj: Dict[str, Any]) -> str:
278 """Format the whole YAML into a RST string"""
279 lines = []
280
281 # Main header
282
283 lines.append(rst_header())
284
285 title = f"Family ``{obj['name']}`` netlink specification"
286 lines.append(rst_title(title))
287 lines.append(rst_paragraph(".. contents::\n"))
288
289 if "doc" in obj:
290 lines.append(rst_subtitle("Summary"))
291 lines.append(rst_paragraph(obj["doc"], 0))
292
293 # Operations
294 if "operations" in obj:
295 lines.append(rst_subtitle("Operations"))
296 lines.append(parse_operations(obj["operations"]["list"]))
297
298 # Multicast groups
299 if "mcast-groups" in obj:
300 lines.append(rst_subtitle("Multicast groups"))
301 lines.append(parse_mcast_group(obj["mcast-groups"]["list"]))
302
303 # Definitions
304 if "definitions" in obj:
305 lines.append(rst_subtitle("Definitions"))
306 lines.append(parse_definitions(obj["definitions"]))
307
308 # Attributes set
309 if "attribute-sets" in obj:
310 lines.append(rst_subtitle("Attribute sets"))
311 lines.append(parse_attr_sets(obj["attribute-sets"]))
312
313 # Sub-messages
314 if "sub-messages" in obj:
315 lines.append(rst_subtitle("Sub-messages"))
316 lines.append(parse_sub_messages(obj["sub-messages"]))
317
318 return "\n".join(lines)
319
320
321# Main functions
322# ==============
323
324
325def parse_arguments() -> argparse.Namespace:
326 """Parse arguments from user"""
327 parser = argparse.ArgumentParser(description="Netlink RST generator")
328
329 parser.add_argument("-v", "--verbose", action="store_true")
330 parser.add_argument("-o", "--output", help="Output file name")
331
332 # Index and input are mutually exclusive
333 group = parser.add_mutually_exclusive_group()
334 group.add_argument(
335 "-x", "--index", action="store_true", help="Generate the index page"
336 )
337 group.add_argument("-i", "--input", help="YAML file name")
338
339 args = parser.parse_args()
340
341 if args.verbose:
342 logging.basicConfig(level=logging.DEBUG)
343
344 if args.input and not os.path.isfile(args.input):
345 logging.warning("%s is not a valid file.", args.input)
346 sys.exit(-1)
347
348 if not args.output:
349 logging.error("No output file specified.")
350 sys.exit(-1)
351
352 if os.path.isfile(args.output):
353 logging.debug("%s already exists. Overwriting it.", args.output)
354
355 return args
356
357
358def parse_yaml_file(filename: str) -> str:
359 """Transform the YAML specified by filename into a rst-formmated string"""
360 with open(filename, "r", encoding="utf-8") as spec_file:
361 yaml_data = yaml.safe_load(spec_file)
362 content = parse_yaml(yaml_data)
363
364 return content
365
366
367def write_to_rstfile(content: str, filename: str) -> None:
368 """Write the generated content into an RST file"""
369 logging.debug("Saving RST file to %s", filename)
370
371 with open(filename, "w", encoding="utf-8") as rst_file:
372 rst_file.write(content)
373
374
375def generate_main_index_rst(output: str) -> None:
376 """Generate the `networking_spec/index` content and write to the file"""
377 lines = []
378
379 lines.append(rst_header())
380 lines.append(rst_label("specs"))
381 lines.append(rst_title("Netlink Family Specifications"))
382 lines.append(rst_toctree(1))
383
384 index_dir = os.path.dirname(output)
385 logging.debug("Looking for .rst files in %s", index_dir)
386 for filename in sorted(os.listdir(index_dir)):
387 if not filename.endswith(".rst") or filename == "index.rst":
388 continue
389 lines.append(f" {filename.replace('.rst', '')}\n")
390
391 logging.debug("Writing an index file at %s", output)
392 write_to_rstfile("".join(lines), output)
393
394
395def main() -> None:
396 """Main function that reads the YAML files and generates the RST files"""
397
398 args = parse_arguments()
399
400 if args.input:
401 logging.debug("Parsing %s", args.input)
402 try:
403 content = parse_yaml_file(os.path.join(args.input))
404 except Exception as exception:
405 logging.warning("Failed to parse %s.", args.input)
406 logging.warning(exception)
407 sys.exit(-1)
408
409 write_to_rstfile(content, args.output)
410
411 if args.index:
412 # Generate the index RST file
413 generate_main_index_rst(args.output)
414
415
416if __name__ == "__main__":
417 main()
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# -*- coding: utf-8; mode: python -*-
4
5"""
6 Script to auto generate the documentation for Netlink specifications.
7
8 :copyright: Copyright (C) 2023 Breno Leitao <leitao@debian.org>
9 :license: GPL Version 2, June 1991 see linux/COPYING for details.
10
11 This script performs extensive parsing to the Linux kernel's netlink YAML
12 spec files, in an effort to avoid needing to heavily mark up the original
13 YAML file.
14
15 This code is split in three big parts:
16 1) RST formatters: Use to convert a string to a RST output
17 2) Parser helpers: Functions to parse the YAML data structure
18 3) Main function and small helpers
19"""
20
21from typing import Any, Dict, List
22import os.path
23import sys
24import argparse
25import logging
26import yaml
27
28
29SPACE_PER_LEVEL = 4
30
31
32# RST Formatters
33# ==============
34def headroom(level: int) -> str:
35 """Return space to format"""
36 return " " * (level * SPACE_PER_LEVEL)
37
38
39def bold(text: str) -> str:
40 """Format bold text"""
41 return f"**{text}**"
42
43
44def inline(text: str) -> str:
45 """Format inline text"""
46 return f"``{text}``"
47
48
49def sanitize(text: str) -> str:
50 """Remove newlines and multiple spaces"""
51 # This is useful for some fields that are spread across multiple lines
52 return str(text).replace("\n", " ").strip()
53
54
55def rst_fields(key: str, value: str, level: int = 0) -> str:
56 """Return a RST formatted field"""
57 return headroom(level) + f":{key}: {value}"
58
59
60def rst_definition(key: str, value: Any, level: int = 0) -> str:
61 """Format a single rst definition"""
62 return headroom(level) + key + "\n" + headroom(level + 1) + str(value)
63
64
65def rst_paragraph(paragraph: str, level: int = 0) -> str:
66 """Return a formatted paragraph"""
67 return headroom(level) + paragraph
68
69
70def rst_bullet(item: str, level: int = 0) -> str:
71 """Return a formatted a bullet"""
72 return headroom(level) + f"- {item}"
73
74
75def rst_subsection(title: str) -> str:
76 """Add a sub-section to the document"""
77 return f"{title}\n" + "-" * len(title)
78
79
80def rst_subsubsection(title: str) -> str:
81 """Add a sub-sub-section to the document"""
82 return f"{title}\n" + "~" * len(title)
83
84
85def rst_section(namespace: str, prefix: str, title: str) -> str:
86 """Add a section to the document"""
87 return f".. _{namespace}-{prefix}-{title}:\n\n{title}\n" + "=" * len(title)
88
89
90def rst_subtitle(title: str) -> str:
91 """Add a subtitle to the document"""
92 return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n"
93
94
95def rst_title(title: str) -> str:
96 """Add a title to the document"""
97 return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n"
98
99
100def rst_list_inline(list_: List[str], level: int = 0) -> str:
101 """Format a list using inlines"""
102 return headroom(level) + "[" + ", ".join(inline(i) for i in list_) + "]"
103
104
105def rst_ref(namespace: str, prefix: str, name: str) -> str:
106 """Add a hyperlink to the document"""
107 mappings = {'enum': 'definition',
108 'fixed-header': 'definition',
109 'nested-attributes': 'attribute-set',
110 'struct': 'definition'}
111 if prefix in mappings:
112 prefix = mappings[prefix]
113 return f":ref:`{namespace}-{prefix}-{name}`"
114
115
116def rst_header() -> str:
117 """The headers for all the auto generated RST files"""
118 lines = []
119
120 lines.append(rst_paragraph(".. SPDX-License-Identifier: GPL-2.0"))
121 lines.append(rst_paragraph(".. NOTE: This document was auto-generated.\n\n"))
122
123 return "\n".join(lines)
124
125
126def rst_toctree(maxdepth: int = 2) -> str:
127 """Generate a toctree RST primitive"""
128 lines = []
129
130 lines.append(".. toctree::")
131 lines.append(f" :maxdepth: {maxdepth}\n\n")
132
133 return "\n".join(lines)
134
135
136def rst_label(title: str) -> str:
137 """Return a formatted label"""
138 return f".. _{title}:\n\n"
139
140
141# Parsers
142# =======
143
144
145def parse_mcast_group(mcast_group: List[Dict[str, Any]]) -> str:
146 """Parse 'multicast' group list and return a formatted string"""
147 lines = []
148 for group in mcast_group:
149 lines.append(rst_bullet(group["name"]))
150
151 return "\n".join(lines)
152
153
154def parse_do(do_dict: Dict[str, Any], level: int = 0) -> str:
155 """Parse 'do' section and return a formatted string"""
156 lines = []
157 for key in do_dict.keys():
158 lines.append(rst_paragraph(bold(key), level + 1))
159 if key in ['request', 'reply']:
160 lines.append(parse_do_attributes(do_dict[key], level + 1) + "\n")
161 else:
162 lines.append(headroom(level + 2) + do_dict[key] + "\n")
163
164 return "\n".join(lines)
165
166
167def parse_do_attributes(attrs: Dict[str, Any], level: int = 0) -> str:
168 """Parse 'attributes' section"""
169 if "attributes" not in attrs:
170 return ""
171 lines = [rst_fields("attributes", rst_list_inline(attrs["attributes"]), level + 1)]
172
173 return "\n".join(lines)
174
175
176def parse_operations(operations: List[Dict[str, Any]], namespace: str) -> str:
177 """Parse operations block"""
178 preprocessed = ["name", "doc", "title", "do", "dump", "flags"]
179 linkable = ["fixed-header", "attribute-set"]
180 lines = []
181
182 for operation in operations:
183 lines.append(rst_section(namespace, 'operation', operation["name"]))
184 lines.append(rst_paragraph(operation["doc"]) + "\n")
185
186 for key in operation.keys():
187 if key in preprocessed:
188 # Skip the special fields
189 continue
190 value = operation[key]
191 if key in linkable:
192 value = rst_ref(namespace, key, value)
193 lines.append(rst_fields(key, value, 0))
194 if 'flags' in operation:
195 lines.append(rst_fields('flags', rst_list_inline(operation['flags'])))
196
197 if "do" in operation:
198 lines.append(rst_paragraph(":do:", 0))
199 lines.append(parse_do(operation["do"], 0))
200 if "dump" in operation:
201 lines.append(rst_paragraph(":dump:", 0))
202 lines.append(parse_do(operation["dump"], 0))
203
204 # New line after fields
205 lines.append("\n")
206
207 return "\n".join(lines)
208
209
210def parse_entries(entries: List[Dict[str, Any]], level: int) -> str:
211 """Parse a list of entries"""
212 ignored = ["pad"]
213 lines = []
214 for entry in entries:
215 if isinstance(entry, dict):
216 # entries could be a list or a dictionary
217 field_name = entry.get("name", "")
218 if field_name in ignored:
219 continue
220 type_ = entry.get("type")
221 if type_:
222 field_name += f" ({inline(type_)})"
223 lines.append(
224 rst_fields(field_name, sanitize(entry.get("doc", "")), level)
225 )
226 elif isinstance(entry, list):
227 lines.append(rst_list_inline(entry, level))
228 else:
229 lines.append(rst_bullet(inline(sanitize(entry)), level))
230
231 lines.append("\n")
232 return "\n".join(lines)
233
234
235def parse_definitions(defs: Dict[str, Any], namespace: str) -> str:
236 """Parse definitions section"""
237 preprocessed = ["name", "entries", "members"]
238 ignored = ["render-max"] # This is not printed
239 lines = []
240
241 for definition in defs:
242 lines.append(rst_section(namespace, 'definition', definition["name"]))
243 for k in definition.keys():
244 if k in preprocessed + ignored:
245 continue
246 lines.append(rst_fields(k, sanitize(definition[k]), 0))
247
248 # Field list needs to finish with a new line
249 lines.append("\n")
250 if "entries" in definition:
251 lines.append(rst_paragraph(":entries:", 0))
252 lines.append(parse_entries(definition["entries"], 1))
253 if "members" in definition:
254 lines.append(rst_paragraph(":members:", 0))
255 lines.append(parse_entries(definition["members"], 1))
256
257 return "\n".join(lines)
258
259
260def parse_attr_sets(entries: List[Dict[str, Any]], namespace: str) -> str:
261 """Parse attribute from attribute-set"""
262 preprocessed = ["name", "type"]
263 linkable = ["enum", "nested-attributes", "struct", "sub-message"]
264 ignored = ["checks"]
265 lines = []
266
267 for entry in entries:
268 lines.append(rst_section(namespace, 'attribute-set', entry["name"]))
269 for attr in entry["attributes"]:
270 type_ = attr.get("type")
271 attr_line = attr["name"]
272 if type_:
273 # Add the attribute type in the same line
274 attr_line += f" ({inline(type_)})"
275
276 lines.append(rst_subsubsection(attr_line))
277
278 for k in attr.keys():
279 if k in preprocessed + ignored:
280 continue
281 if k in linkable:
282 value = rst_ref(namespace, k, attr[k])
283 else:
284 value = sanitize(attr[k])
285 lines.append(rst_fields(k, value, 0))
286 lines.append("\n")
287
288 return "\n".join(lines)
289
290
291def parse_sub_messages(entries: List[Dict[str, Any]], namespace: str) -> str:
292 """Parse sub-message definitions"""
293 lines = []
294
295 for entry in entries:
296 lines.append(rst_section(namespace, 'sub-message', entry["name"]))
297 for fmt in entry["formats"]:
298 value = fmt["value"]
299
300 lines.append(rst_bullet(bold(value)))
301 for attr in ['fixed-header', 'attribute-set']:
302 if attr in fmt:
303 lines.append(rst_fields(attr,
304 rst_ref(namespace, attr, fmt[attr]),
305 1))
306 lines.append("\n")
307
308 return "\n".join(lines)
309
310
311def parse_yaml(obj: Dict[str, Any]) -> str:
312 """Format the whole YAML into a RST string"""
313 lines = []
314
315 # Main header
316
317 lines.append(rst_header())
318
319 family = obj['name']
320
321 title = f"Family ``{family}`` netlink specification"
322 lines.append(rst_title(title))
323 lines.append(rst_paragraph(".. contents:: :depth: 3\n"))
324
325 if "doc" in obj:
326 lines.append(rst_subtitle("Summary"))
327 lines.append(rst_paragraph(obj["doc"], 0))
328
329 # Operations
330 if "operations" in obj:
331 lines.append(rst_subtitle("Operations"))
332 lines.append(parse_operations(obj["operations"]["list"], family))
333
334 # Multicast groups
335 if "mcast-groups" in obj:
336 lines.append(rst_subtitle("Multicast groups"))
337 lines.append(parse_mcast_group(obj["mcast-groups"]["list"]))
338
339 # Definitions
340 if "definitions" in obj:
341 lines.append(rst_subtitle("Definitions"))
342 lines.append(parse_definitions(obj["definitions"], family))
343
344 # Attributes set
345 if "attribute-sets" in obj:
346 lines.append(rst_subtitle("Attribute sets"))
347 lines.append(parse_attr_sets(obj["attribute-sets"], family))
348
349 # Sub-messages
350 if "sub-messages" in obj:
351 lines.append(rst_subtitle("Sub-messages"))
352 lines.append(parse_sub_messages(obj["sub-messages"], family))
353
354 return "\n".join(lines)
355
356
357# Main functions
358# ==============
359
360
361def parse_arguments() -> argparse.Namespace:
362 """Parse arguments from user"""
363 parser = argparse.ArgumentParser(description="Netlink RST generator")
364
365 parser.add_argument("-v", "--verbose", action="store_true")
366 parser.add_argument("-o", "--output", help="Output file name")
367
368 # Index and input are mutually exclusive
369 group = parser.add_mutually_exclusive_group()
370 group.add_argument(
371 "-x", "--index", action="store_true", help="Generate the index page"
372 )
373 group.add_argument("-i", "--input", help="YAML file name")
374
375 args = parser.parse_args()
376
377 if args.verbose:
378 logging.basicConfig(level=logging.DEBUG)
379
380 if args.input and not os.path.isfile(args.input):
381 logging.warning("%s is not a valid file.", args.input)
382 sys.exit(-1)
383
384 if not args.output:
385 logging.error("No output file specified.")
386 sys.exit(-1)
387
388 if os.path.isfile(args.output):
389 logging.debug("%s already exists. Overwriting it.", args.output)
390
391 return args
392
393
394def parse_yaml_file(filename: str) -> str:
395 """Transform the YAML specified by filename into a rst-formmated string"""
396 with open(filename, "r", encoding="utf-8") as spec_file:
397 yaml_data = yaml.safe_load(spec_file)
398 content = parse_yaml(yaml_data)
399
400 return content
401
402
403def write_to_rstfile(content: str, filename: str) -> None:
404 """Write the generated content into an RST file"""
405 logging.debug("Saving RST file to %s", filename)
406
407 with open(filename, "w", encoding="utf-8") as rst_file:
408 rst_file.write(content)
409
410
411def generate_main_index_rst(output: str) -> None:
412 """Generate the `networking_spec/index` content and write to the file"""
413 lines = []
414
415 lines.append(rst_header())
416 lines.append(rst_label("specs"))
417 lines.append(rst_title("Netlink Family Specifications"))
418 lines.append(rst_toctree(1))
419
420 index_dir = os.path.dirname(output)
421 logging.debug("Looking for .rst files in %s", index_dir)
422 for filename in sorted(os.listdir(index_dir)):
423 if not filename.endswith(".rst") or filename == "index.rst":
424 continue
425 lines.append(f" {filename.replace('.rst', '')}\n")
426
427 logging.debug("Writing an index file at %s", output)
428 write_to_rstfile("".join(lines), output)
429
430
431def main() -> None:
432 """Main function that reads the YAML files and generates the RST files"""
433
434 args = parse_arguments()
435
436 if args.input:
437 logging.debug("Parsing %s", args.input)
438 try:
439 content = parse_yaml_file(os.path.join(args.input))
440 except Exception as exception:
441 logging.warning("Failed to parse %s.", args.input)
442 logging.warning(exception)
443 sys.exit(-1)
444
445 write_to_rstfile(content, args.output)
446
447 if args.index:
448 # Generate the index RST file
449 generate_main_index_rst(args.output)
450
451
452if __name__ == "__main__":
453 main()