#!/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 ]", # .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 + "]"), adjacent=False) 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