Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import sys |
| 4 | import os |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 5 | import os.path |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 6 | import ipaddress |
| 7 | import yaml |
| 8 | from pprint import pprint |
| 9 | import re |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 10 | from jsonschema import validate, exceptions |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 11 | import argparse |
| 12 | from subprocess import run, PIPE |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 13 | from io import StringIO |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 14 | import urllib.parse |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 15 | |
| 16 | # VPP feature JSON schema |
| 17 | schema = { |
| 18 | "$schema": "http://json-schema.org/schema#", |
| 19 | "type": "object", |
| 20 | "properties": { |
| 21 | "name": {"type": "string"}, |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 22 | "description": {"type": "string"}, |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 23 | "maintainer": {"$ref": "#/definitions/maintainers"}, |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 24 | "state": { |
| 25 | "type": "string", |
| 26 | "enum": ["production", "experimental", "development"], |
| 27 | }, |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 28 | "features": {"$ref": "#/definitions/features"}, |
| 29 | "missing": {"$ref": "#/definitions/features"}, |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 30 | "properties": { |
| 31 | "type": "array", |
| 32 | "items": {"type": "string", "enum": ["API", "CLI", "STATS", "MULTITHREAD"]}, |
| 33 | }, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 34 | }, |
| 35 | "additionalProperties": False, |
| 36 | "definitions": { |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 37 | "maintainers": { |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 38 | "anyof": [ |
| 39 | { |
| 40 | "type": "array", |
| 41 | "items": {"type": "string"}, |
| 42 | "minItems": 1, |
| 43 | }, |
| 44 | {"type": "string"}, |
| 45 | ], |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 46 | }, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 47 | "featureobject": { |
| 48 | "type": "object", |
| 49 | "patternProperties": { |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 50 | "^.*$": {"$ref": "#/definitions/features"}, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 51 | }, |
| 52 | }, |
| 53 | "features": { |
| 54 | "type": "array", |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 55 | "items": { |
| 56 | "anyOf": [ |
| 57 | {"$ref": "#/definitions/featureobject"}, |
| 58 | {"type": "string"}, |
| 59 | ] |
| 60 | }, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 61 | "minItems": 1, |
| 62 | }, |
| 63 | }, |
| 64 | } |
| 65 | |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 66 | DEFAULT_REPO_LINK = "https://github.com/FDio/vpp/blob/master/" |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 67 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 68 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 69 | def filelist_from_git_status(): |
| 70 | filelist = [] |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 71 | git_status = "git status --porcelain */FEATURE*.yaml" |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 72 | rv = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| 73 | if rv.returncode != 0: |
| 74 | sys.exit(rv.returncode) |
| 75 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 76 | for l in rv.stdout.decode("ascii").split("\n"): |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 77 | if len(l): |
| 78 | filelist.append(l.split()[1]) |
| 79 | return filelist |
| 80 | |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 81 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 82 | def filelist_from_git_ls(): |
| 83 | filelist = [] |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 84 | git_ls = "git ls-files :(top)*/FEATURE*.yaml" |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 85 | rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| 86 | if rv.returncode != 0: |
| 87 | sys.exit(rv.returncode) |
| 88 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 89 | for l in rv.stdout.decode("ascii").split("\n"): |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 90 | if len(l): |
| 91 | filelist.append(l) |
| 92 | return filelist |
| 93 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 94 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 95 | def version_from_git(): |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 96 | git_describe = "git describe" |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 97 | rv = run(git_describe.split(), stdout=PIPE, stderr=PIPE) |
| 98 | if rv.returncode != 0: |
| 99 | sys.exit(rv.returncode) |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 100 | return rv.stdout.decode("ascii").split("\n")[0] |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 101 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 102 | |
| 103 | class MarkDown: |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 104 | _dispatch = {} |
| 105 | |
| 106 | def __init__(self, stream): |
| 107 | self.stream = stream |
| 108 | self.toc = [] |
| 109 | |
| 110 | def print_maintainer(self, o): |
| 111 | write = self.stream.write |
| 112 | if type(o) is list: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 113 | write("Maintainers: " + ", ".join("{m}".format(m=m) for m in o) + " \n") |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 114 | else: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 115 | write("Maintainer: {o} \n".format(o=o)) |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 116 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 117 | _dispatch["maintainer"] = print_maintainer |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 118 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 119 | def print_features(self, o, indent=0): |
| 120 | write = self.stream.write |
| 121 | for f in o: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 122 | indentstr = " " * indent |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 123 | if type(f) is dict: |
| 124 | for k, v in f.items(): |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 125 | write("{indentstr}- {k}\n".format(indentstr=indentstr, k=k)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 126 | self.print_features(v, indent + 2) |
| 127 | else: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 128 | write("{indentstr}- {f}\n".format(indentstr=indentstr, f=f)) |
| 129 | write("\n") |
| 130 | |
| 131 | _dispatch["features"] = print_features |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 132 | |
| 133 | def print_markdown_header(self, o): |
| 134 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 135 | write("## {o}\n".format(o=o)) |
| 136 | |
| 137 | _dispatch["markdown_header"] = print_markdown_header |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 138 | |
| 139 | def print_name(self, o): |
| 140 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 141 | write("### {o}\n".format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 142 | self.toc.append(o) |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 143 | |
| 144 | _dispatch["name"] = print_name |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 145 | |
| 146 | def print_description(self, o): |
| 147 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 148 | write("\n{o}\n\n".format(o=o)) |
| 149 | |
| 150 | _dispatch["description"] = print_description |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 151 | |
| 152 | def print_state(self, o): |
| 153 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 154 | write("Feature maturity level: {o} \n".format(o=o)) |
| 155 | |
| 156 | _dispatch["state"] = print_state |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 157 | |
| 158 | def print_properties(self, o): |
| 159 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 160 | write("Supports: {s} \n".format(s=" ".join(o))) |
| 161 | |
| 162 | _dispatch["properties"] = print_properties |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 163 | |
| 164 | def print_missing(self, o): |
| 165 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 166 | write("\nNot yet implemented: \n") |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 167 | self.print_features(o) |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 168 | |
| 169 | _dispatch["missing"] = print_missing |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 170 | |
| 171 | def print_code(self, o): |
| 172 | write = self.stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 173 | write("Source Code: [{o}]({o}) \n".format(o=o)) |
| 174 | |
| 175 | _dispatch["code"] = print_code |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 176 | |
| 177 | def print(self, t, o): |
| 178 | write = self.stream.write |
| 179 | if t in self._dispatch: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 180 | self._dispatch[t]( |
| 181 | self, |
| 182 | o, |
| 183 | ) |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 184 | else: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 185 | write("NOT IMPLEMENTED: {t}\n") |
| 186 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 187 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 188 | def output_toc(toc, stream): |
| 189 | write = stream.write |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 190 | write("# VPP Supported Features\n") |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 191 | |
| 192 | for t in toc: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 193 | ref = t.lower().replace(" ", "-") |
| 194 | write("[{t}](#{ref}) \n".format(t=t, ref=ref)) |
| 195 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 196 | |
| 197 | def featuresort(k): |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 198 | return k[1]["name"] |
| 199 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 200 | |
| 201 | def featurelistsort(k): |
| 202 | orderedfields = { |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 203 | "name": 0, |
| 204 | "maintainer": 1, |
| 205 | "description": 2, |
| 206 | "features": 3, |
| 207 | "state": 4, |
| 208 | "properties": 5, |
| 209 | "missing": 6, |
| 210 | "code": 7, |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 211 | } |
| 212 | return orderedfields[k[0]] |
| 213 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 214 | |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 215 | def output_markdown(features, fields, notfields, repository_url): |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 216 | stream = StringIO() |
| 217 | m = MarkDown(stream) |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 218 | m.print("markdown_header", "Feature Details:") |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 219 | for path, featuredef in sorted(features.items(), key=featuresort): |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 220 | codeurl = urllib.parse.urljoin(repository_url, os.path.dirname(path)) |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 221 | featuredef["code"] = codeurl |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 222 | for k, v in sorted(featuredef.items(), key=featurelistsort): |
| 223 | if notfields: |
| 224 | if k not in notfields: |
| 225 | m.print(k, v) |
| 226 | elif fields: |
| 227 | if k in fields: |
| 228 | m.print(k, v) |
| 229 | else: |
| 230 | m.print(k, v) |
| 231 | |
| 232 | tocstream = StringIO() |
| 233 | output_toc(m.toc, tocstream) |
| 234 | return tocstream, stream |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 235 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 236 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 237 | def main(): |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 238 | parser = argparse.ArgumentParser(description="VPP Feature List.") |
| 239 | parser.add_argument( |
| 240 | "--validate", |
| 241 | dest="validate", |
| 242 | action="store_true", |
| 243 | help="validate the FEATURE.yaml file", |
| 244 | ) |
| 245 | parser.add_argument( |
| 246 | "--repolink", |
| 247 | metavar="repolink", |
| 248 | default=DEFAULT_REPO_LINK, |
| 249 | help="Link to public repository [%s]" % DEFAULT_REPO_LINK, |
| 250 | ) |
| 251 | parser.add_argument( |
| 252 | "--git-status", |
| 253 | dest="git_status", |
| 254 | action="store_true", |
| 255 | help="Get filelist from git status", |
| 256 | ) |
| 257 | parser.add_argument( |
| 258 | "--all", |
| 259 | dest="all", |
| 260 | action="store_true", |
| 261 | help="Validate all files in repository", |
| 262 | ) |
| 263 | parser.add_argument( |
| 264 | "--markdown", |
| 265 | dest="markdown", |
| 266 | action="store_true", |
| 267 | help="Output feature table in markdown", |
| 268 | ) |
| 269 | parser.add_argument( |
| 270 | "infile", nargs="?", type=argparse.FileType("r"), default=sys.stdin |
| 271 | ) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 272 | group = parser.add_mutually_exclusive_group() |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 273 | group.add_argument("--include", help="List of fields to include") |
| 274 | group.add_argument("--exclude", help="List of fields to exclude") |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 275 | args = parser.parse_args() |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 276 | features = {} |
| 277 | |
| 278 | if args.git_status: |
| 279 | filelist = filelist_from_git_status() |
| 280 | elif args.all: |
| 281 | filelist = filelist_from_git_ls() |
| 282 | else: |
| 283 | filelist = args.infile |
| 284 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 285 | if args.include: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 286 | fields = args.include.split(",") |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 287 | else: |
| 288 | fields = [] |
| 289 | if args.exclude: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 290 | notfields = args.exclude.split(",") |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 291 | else: |
| 292 | notfields = [] |
| 293 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 294 | for featurefile in filelist: |
| 295 | featurefile = featurefile.rstrip() |
| 296 | |
| 297 | # Load configuration file |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 298 | with open(featurefile, encoding="utf-8") as f: |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 299 | cfg = yaml.load(f, Loader=yaml.SafeLoader) |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 300 | try: |
| 301 | validate(instance=cfg, schema=schema) |
| 302 | except exceptions.ValidationError: |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 303 | print( |
| 304 | "File does not validate: {featurefile}".format(featurefile=featurefile), |
| 305 | file=sys.stderr, |
| 306 | ) |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 307 | raise |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 308 | features[featurefile] = cfg |
| 309 | |
| 310 | if args.markdown: |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 311 | stream = StringIO() |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 312 | tocstream, stream = output_markdown(features, fields, notfields, args.repolink) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 313 | print(tocstream.getvalue()) |
| 314 | print(stream.getvalue()) |
| 315 | stream.close() |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 316 | |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 317 | |
Klement Sekera | d9b0c6f | 2022-04-26 19:02:15 +0200 | [diff] [blame] | 318 | if __name__ == "__main__": |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 319 | main() |