| #!/usr/bin/env python |
| # Copyright (c) 2016 Comcast Cable Communications Management, LLC. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # Filter for .siphon files that are generated by other filters. |
| # The idea is to siphon off certain initializers so that we can better |
| # auto-document the contents of that initializer. |
| |
| import os, sys, re, argparse, cgi, json |
| import pyparsing as pp |
| |
| import pprint |
| |
| DEFAULT_SIPHON ="clicmd" |
| DEFAULT_OUTPUT = None |
| DEFAULT_PREFIX = os.getcwd() |
| |
| siphon_map = { |
| 'clicmd': "VLIB_CLI_COMMAND", |
| } |
| |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--type", '-t', metavar="siphon_type", default=DEFAULT_SIPHON, |
| choices=siphon_map.keys(), |
| help="Siphon type to process [%s]" % DEFAULT_SIPHON) |
| ap.add_argument("--output", '-o', metavar="directory", default=DEFAULT_OUTPUT, |
| help="Output directory for .md files [%s]" % DEFAULT_OUTPUT) |
| ap.add_argument("--input-prefix", metavar="path", default=DEFAULT_PREFIX, |
| help="Prefix to strip from input pathnames [%s]" % DEFAULT_PREFIX) |
| ap.add_argument("input", nargs='+', metavar="input_file", |
| help="Input .siphon files") |
| args = ap.parse_args() |
| |
| if args.output is None: |
| sys.stderr.write("Error: Siphon processor requires --output to be set.") |
| sys.exit(1) |
| |
| |
| def clicmd_index_sort(cfg, group, dec): |
| if group in dec and 'group_label' in dec[group]: |
| return dec[group]['group_label'] |
| return group |
| |
| def clicmd_index_header(cfg): |
| s = "# CLI command index\n" |
| s += "\n[TOC]\n" |
| return s |
| |
| def clicmd_index_section(cfg, group, md): |
| return "\n@subpage %s\n\n" % md |
| |
| def clicmd_index_entry(cfg, meta, item): |
| v = item["value"] |
| return "* [%s](@ref %s)\n" % (v["path"], meta["label"]) |
| |
| def clicmd_sort(cfg, meta, item): |
| return item['value']['path'] |
| |
| def clicmd_header(cfg, group, md, dec): |
| if group in dec and 'group_label' in dec[group]: |
| label = dec[group]['group_label'] |
| else: |
| label = group |
| return "\n@page %s %s\n" % (md, label) |
| |
| def clicmd_format(cfg, meta, item): |
| v = item["value"] |
| s = "\n@section %s %s\n" % (meta['label'], v['path']) |
| |
| # The text from '.short_help = '. |
| # Later we should split this into short_help and usage_help |
| # since the latter is how it is primarily used but the former |
| # is also needed. |
| if "short_help" in v: |
| tmp = v["short_help"].strip() |
| |
| # Bit hacky. Add a trailing period if it doesn't have one. |
| if tmp[-1] != ".": |
| tmp += "." |
| |
| s += "### Summary/usage\n %s\n\n" % tmp |
| |
| # This is seldom used and will likely be deprecated |
| if "long_help" in v: |
| tmp = v["long_help"] |
| |
| s += "### Long help\n %s\n\n" % tmp |
| |
| # Extracted from the code in /*? ... ?*/ blocks |
| if "siphon_block" in item["meta"]: |
| sb = item["meta"]["siphon_block"] |
| |
| if sb != "": |
| # hack. still needed? |
| sb = sb.replace("\n", "\\n") |
| try: |
| sb = json.loads('"'+sb+'"') |
| s += "### Description\n%s\n\n" % sb |
| except: |
| pass |
| |
| # Gives some developer-useful linking |
| if "item" in meta or "function" in v: |
| s += "### Declaration and implementation\n\n" |
| |
| if "item" in meta: |
| s += "Declaration: @ref %s (%s:%d)\n\n" % \ |
| (meta['item'], meta["file"], int(item["meta"]["line_start"])) |
| |
| if "function" in v: |
| s += "Implementation: @ref %s.\n\n" % v["function"] |
| |
| return s |
| |
| |
| siphons = { |
| "VLIB_CLI_COMMAND": { |
| "index_sort_key": clicmd_index_sort, |
| "index_header": clicmd_index_header, |
| "index_section": clicmd_index_section, |
| "index_entry": clicmd_index_entry, |
| 'sort_key': clicmd_sort, |
| "header": clicmd_header, |
| "format": clicmd_format, |
| } |
| } |
| |
| |
| # PyParsing definition for our struct initializers which look like this: |
| # VLIB_CLI_COMMAND (show_sr_tunnel_command, static) = { |
| # .path = "show sr tunnel", |
| # .short_help = "show sr tunnel [name <sr-tunnel-name>]", |
| # .function = show_sr_tunnel_fn, |
| #}; |
| def getMacroInitializerBNF(): |
| cs = pp.Forward() |
| ident = pp.Word(pp.alphas + "_", pp.alphas + pp.nums + "_") |
| intNum = pp.Word(pp.nums) |
| hexNum = pp.Literal("0x") + pp.Word(pp.hexnums) |
| octalNum = pp.Literal("0") + pp.Word("01234567") |
| integer = (hexNum | octalNum | intNum) + \ |
| pp.Optional(pp.Literal("ULL") | pp.Literal("LL") | pp.Literal("L")) |
| floatNum = pp.Regex(r'\d+(\.\d*)?([eE]\d+)?') + pp.Optional(pp.Literal("f")) |
| char = pp.Literal("'") + pp.Word(pp.printables, exact=1) + pp.Literal("'") |
| arrayIndex = integer | ident |
| |
| lbracket = pp.Literal("(").suppress() |
| rbracket = pp.Literal(")").suppress() |
| lbrace = pp.Literal("{").suppress() |
| rbrace = pp.Literal("}").suppress() |
| comma = pp.Literal(",").suppress() |
| equals = pp.Literal("=").suppress() |
| dot = pp.Literal(".").suppress() |
| semicolon = pp.Literal(";").suppress() |
| |
| # initializer := { [member = ] (variable | expression | { initializer } ) } |
| typeName = ident |
| varName = ident |
| |
| typeSpec = pp.Optional("unsigned") + \ |
| pp.oneOf("int long short float double char u8 i8 void") + \ |
| pp.Optional(pp.Word("*"), default="") |
| typeCast = pp.Combine( "(" + ( typeSpec | typeName ) + ")" ).suppress() |
| |
| string = pp.Combine(pp.OneOrMore(pp.QuotedString(quoteChar='"', |
| escChar='\\', multiline=True)), adjacent=False) |
| literal = pp.Optional(typeCast) + (integer | floatNum | char | string) |
| var = pp.Combine(pp.Optional(typeCast) + varName + pp.Optional("[" + arrayIndex + "]")) |
| |
| expr = (literal | var) # TODO |
| |
| |
| member = pp.Combine(dot + varName + pp.Optional("[" + arrayIndex + "]")) |
| value = (expr | cs) |
| |
| entry = pp.Group(pp.Optional(member + equals, default="") + value) |
| entries = (pp.ZeroOrMore(entry + comma) + entry + pp.Optional(comma)) | \ |
| (pp.ZeroOrMore(entry + comma)) |
| |
| cs << (lbrace + entries + rbrace) |
| |
| macroName = ident |
| params = pp.Group(pp.ZeroOrMore(expr + comma) + expr) |
| macroParams = lbracket + params + rbracket |
| |
| mi = macroName + pp.Optional(macroParams) + equals + pp.Group(cs) + semicolon |
| mi.ignore(pp.cppStyleComment) |
| return mi |
| |
| |
| mi = getMacroInitializerBNF() |
| |
| # Parse the input file into a more usable dictionary structure |
| cmds = {} |
| line_num = 0 |
| line_start = 0 |
| for filename in args.input: |
| sys.stderr.write("Parsing items in file \"%s\"...\n" % filename) |
| data = None |
| with open(filename, "r") as fd: |
| data = json.load(fd) |
| |
| cmds['_global'] = data['global'] |
| |
| # iterate the items loaded and regroup it |
| for item in data["items"]: |
| try: |
| o = mi.parseString(item['block']).asList() |
| except: |
| sys.stderr.write("Exception parsing item: %s\n%s\n" \ |
| % (json.dumps(item, separators=(',', ': '), indent=4), |
| item['block'])) |
| raise |
| |
| group = item['group'] |
| file = item['file'] |
| macro = o[0] |
| param = o[1][0] |
| |
| if group not in cmds: |
| cmds[group] = {} |
| |
| if file not in cmds[group]: |
| cmds[group][file] = {} |
| |
| if macro not in cmds[group][file]: |
| cmds[group][file][macro] = {} |
| |
| c = { |
| 'params': o[2], |
| 'meta': {}, |
| 'value': {}, |
| } |
| |
| for key in item: |
| if key == 'block': |
| continue |
| c['meta'][key] = item[key] |
| |
| for i in c['params']: |
| c['value'][i[0]] = cgi.escape(i[1]) |
| |
| cmds[group][file][macro][param] = c |
| |
| |
| # Write the header for this siphon type |
| cfg = siphons[siphon_map[args.type]] |
| sys.stdout.write(cfg["index_header"](cfg)) |
| contents = "" |
| |
| def group_sort_key(item): |
| if "index_sort_key" in cfg: |
| return cfg["index_sort_key"](cfg, item, cmds['_global']) |
| return item |
| |
| # Iterate the dictionary and process it |
| for group in sorted(cmds.keys(), key=group_sort_key): |
| if group.startswith('_'): |
| continue |
| |
| sys.stderr.write("Processing items in group \"%s\"...\n" % group) |
| |
| cfg = siphons[siphon_map[args.type]] |
| md = group.replace("/", "_").replace(".", "_") |
| sys.stdout.write(cfg["index_section"](cfg, group, md)) |
| |
| if "header" in cfg: |
| dec = cmds['_global'] |
| contents += cfg["header"](cfg, group, md, dec) |
| |
| for file in sorted(cmds[group].keys()): |
| if group.startswith('_'): |
| continue |
| |
| sys.stderr.write("- Processing items in file \"%s\"...\n" % file) |
| |
| for macro in sorted(cmds[group][file].keys()): |
| if macro != siphon_map[args.type]: |
| continue |
| sys.stderr.write("-- Processing items in macro \"%s\"...\n" % macro) |
| cfg = siphons[macro] |
| |
| meta = { |
| "group": group, |
| "file": file, |
| "macro": macro, |
| "md": md, |
| } |
| |
| def item_sort_key(item): |
| if "sort_key" in cfg: |
| return cfg["sort_key"](cfg, meta, cmds[group][file][macro][item]) |
| return item |
| |
| for param in sorted(cmds[group][file][macro].keys(), key=item_sort_key): |
| sys.stderr.write("--- Processing item \"%s\"...\n" % param) |
| |
| meta["item"] = param |
| |
| # mangle "md" and the item to make a reference label |
| meta["label"] = "%s___%s" % (meta["md"], param) |
| |
| if "index_entry" in cfg: |
| s = cfg["index_entry"](cfg, meta, cmds[group][file][macro][param]) |
| sys.stdout.write(s) |
| |
| if "format" in cfg: |
| contents += cfg["format"](cfg, meta, cmds[group][file][macro][param]) |
| |
| sys.stdout.write(contents) |
| |
| # All done |