diff options
| author | David Lamparter <equinox@opensourcerouting.org> | 2017-05-11 15:36:04 +0200 |
|---|---|---|
| committer | David Lamparter <equinox@opensourcerouting.org> | 2017-06-14 19:29:26 +0200 |
| commit | 5578a14d949d89e25ec3e6136158603049e5a2dd (patch) | |
| tree | fc1df6006271fb7f73cf8802c4022b266e0344b1 /python | |
| parent | 29ad6f6882de5a871d6d81c609e8b20d825d6138 (diff) | |
python: clidef.py
Adds "DEFPY()" which invokes an additional layer of preprocessing, so
that we get pre-parsed and named function arguments for the CLI.
Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
Diffstat (limited to 'python')
| -rw-r--r-- | python/Makefile.am | 3 | ||||
| -rw-r--r-- | python/clidef.py | 254 | ||||
| -rw-r--r-- | python/clippy/__init__.py | 64 |
3 files changed, 321 insertions, 0 deletions
diff --git a/python/Makefile.am b/python/Makefile.am new file mode 100644 index 0000000000..4ad1e36b59 --- /dev/null +++ b/python/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = \ + clidef.py \ + clippy/__init__.py diff --git a/python/clidef.py b/python/clidef.py new file mode 100644 index 0000000000..de3a764a0b --- /dev/null +++ b/python/clidef.py @@ -0,0 +1,254 @@ +# FRR CLI preprocessor (DEFPY) +# +# Copyright (C) 2017 David Lamparter for NetDEF, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import clippy, traceback, sys, os +from collections import OrderedDict +from functools import reduce +from pprint import pprint +from string import Template +from io import StringIO + +# the various handlers generate output C code for a particular type of +# CLI token, choosing the most useful output C type. + +class RenderHandler(object): + def __init__(self, token): + pass + def combine(self, other): + if type(self) == type(other): + return other + return StringHandler(None) + + deref = '' + drop_str = False + +class StringHandler(RenderHandler): + argtype = 'const char *' + decl = Template('const char *$varname = NULL;') + code = Template('$varname = argv[_i]->arg;') + drop_str = True + +class LongHandler(RenderHandler): + argtype = 'long' + decl = Template('long $varname = 0;') + code = Template('''\ +char *_end; +$varname = strtol(argv[_i]->arg, &_end, 10); +_fail = (_end == argv[_i]->arg) || (*_end != '\\0');''') + +# A.B.C.D/M (prefix_ipv4) and +# X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a +# struct prefix: + +class PrefixBase(RenderHandler): + def combine(self, other): + if type(self) == type(other): + return other + if type(other) in [Prefix4Handler, Prefix6Handler, PrefixGenHandler]: + return PrefixGenHandler(None) + return StringHandler(None) + deref = '&' +class Prefix4Handler(PrefixBase): + argtype = 'const struct prefix_ipv4 *' + decl = Template('struct prefix_ipv4 $varname = { };') + code = Template('_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);') +class Prefix6Handler(PrefixBase): + argtype = 'const struct prefix_ipv6 *' + decl = Template('struct prefix_ipv6 $varname = { };') + code = Template('_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);') +class PrefixGenHandler(PrefixBase): + argtype = 'const struct prefix *' + decl = Template('struct prefix $varname = { };') + code = Template('_fail = !str2prefix(argv[_i]->arg, &$varname);') + +# same for IP addresses. result is union sockunion. +class IPBase(RenderHandler): + def combine(self, other): + if type(self) == type(other): + return other + if type(other) in [IP4Handler, IP6Handler, IPGenHandler]: + return IPGenHandler(None) + return StringHandler(None) +class IP4Handler(IPBase): + argtype = 'struct in_addr' + decl = Template('struct in_addr $varname = { INADDR_ANY };') + code = Template('_fail = !inet_aton(argv[_i]->arg, &$varname);') +class IP6Handler(IPBase): + argtype = 'struct in6_addr' + decl = Template('struct in6_addr $varname = IN6ADDR_ANY_INIT;') + code = Template('_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);') +class IPGenHandler(IPBase): + argtype = 'const union sockunion *' + decl = Template('''union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;''') + code = Template('''\ +if (argv[_i]->text[0] == 'X') { + s__$varname.sa.sa_family = AF_INET6; + _fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr); + $varname = &s__$varname; +} else { + s__$varname.sa.sa_family = AF_INET; + _fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr); + $varname = &s__$varname; +}''') + +def mix_handlers(handlers): + def combine(a, b): + if a is None: + return b + return a.combine(b) + return reduce(combine, handlers, None) + +handlers = { + 'WORD_TKN': StringHandler, + 'VARIABLE_TKN': StringHandler, + 'RANGE_TKN': LongHandler, + 'IPV4_TKN': IP4Handler, + 'IPV4_PREFIX_TKN': Prefix4Handler, + 'IPV6_TKN': IP6Handler, + 'IPV6_PREFIX_TKN': Prefix6Handler, +} + +# core template invoked for each occurence of DEFPY. +templ = Template('''/* $fnname => "$cmddef" */ +DEFUN_CMD_FUNC_DECL($fnname) +#define funcdecl_$fnname static int ${fnname}_magic(\\ + const struct cmd_element *self __attribute__ ((unused)),\\ + struct vty *vty __attribute__ ((unused)),\\ + int argc __attribute__ ((unused)),\\ + struct cmd_token *argv[] __attribute__ ((unused))$argdefs) +funcdecl_$fnname; +DEFUN_CMD_FUNC_TEXT($fnname) +{ + int _i; + unsigned _fail = 0, _failcnt = 0; +$argdecls + for (_i = 0; _i < argc; _i++) { + if (!argv[_i]->varname) + continue; + _fail = 0;$argblocks + if (_fail) + vty_out (vty, "%% invalid input for %s: %s%s", + argv[_i]->varname, argv[_i]->arg, VTY_NEWLINE); + _failcnt += _fail; + } + if (_failcnt) + return CMD_WARNING; + return ${fnname}_magic(self, vty, argc, argv$arglist); +} + +''') + +# invoked for each named parameter +argblock = Template(''' + if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock + $code + }''') + +def process_file(fn, ofd, dumpfd, all_defun): + filedata = clippy.parse(fn) + + for entry in filedata['data']: + if entry['type'] == 'DEFPY' or (all_defun and entry['type'].startswith('DEFUN')): + cmddef = entry['args'][2] + for i in cmddef: + assert i.startswith('"') and i.endswith('"') + cmddef = ''.join([i[1:-1] for i in cmddef]) + + graph = clippy.Graph(cmddef) + args = OrderedDict() + for token, depth in clippy.graph_iterate(graph): + if token.type not in handlers: + continue + if token.varname is None: + continue + arg = args.setdefault(token.varname, []) + arg.append(handlers[token.type](token)) + + #print('-' * 76) + #pprint(entry) + #clippy.dump(graph) + #pprint(args) + + params = { 'cmddef': cmddef, 'fnname': entry['args'][0][0] } + argdefs = [] + argdecls = [] + arglist = [] + argblocks = [] + doc = [] + + def do_add(handler, varname, attr = ''): + argdefs.append(',\\\n\t%s %s%s' % (handler.argtype, varname, attr)) + argdecls.append('\t%s\n' % (handler.decl.substitute({'varname': varname}).replace('\n', '\n\t'))) + arglist.append(', %s%s' % (handler.deref, varname)) + if attr == '': + at = handler.argtype + if not at.startswith('const '): + at = '. . . ' + at + doc.append('\t%-26s %s' % (at, varname)) + + for varname in args.keys(): + handler = mix_handlers(args[varname]) + #print(varname, handler) + if handler is None: continue + do_add(handler, varname) + code = handler.code.substitute({'varname': varname}).replace('\n', '\n\t\t\t') + strblock = '' + if not handler.drop_str: + do_add(StringHandler(None), '%s_str' % (varname), ' __attribute__ ((unused))') + strblock = '\n\t\t\t%s_str = argv[_i]->arg;' % (varname) + argblocks.append(argblock.substitute({'varname': varname, 'strblock': strblock, 'code': code})) + + if dumpfd is not None: + if len(arglist) > 0: + dumpfd.write('"%s":\n%s\n\n' % (cmddef, '\n'.join(doc))) + else: + dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef)) + + params['argdefs'] = ''.join(argdefs) + params['argdecls'] = ''.join(argdecls) + params['arglist'] = ''.join(arglist) + params['argblocks'] = ''.join(argblocks) + ofd.write(templ.substitute(params)) + +if __name__ == '__main__': + import argparse + + argp = argparse.ArgumentParser(description = 'FRR CLI preprocessor in Python') + argp.add_argument('--all-defun', action = 'store_const', const = True, + help = 'process DEFUN() statements in addition to DEFPY()') + argp.add_argument('--show', action = 'store_const', const = True, + help = 'print out list of arguments and types for each definition') + argp.add_argument('-o', type = str, metavar = 'OUTFILE', + help = 'output C file name') + argp.add_argument('cfile', type = str) + args = argp.parse_args() + + dumpfd = None + if args.o is not None: + ofd = StringIO() + if args.show: + dumpfd = sys.stdout + else: + ofd = sys.stdout + if args.show: + dumpfd = sys.stderr + + process_file(args.cfile, ofd, dumpfd, args.all_defun) + + if args.o is not None: + clippy.wrdiff(args.o, ofd) diff --git a/python/clippy/__init__.py b/python/clippy/__init__.py new file mode 100644 index 0000000000..82aa9495d4 --- /dev/null +++ b/python/clippy/__init__.py @@ -0,0 +1,64 @@ +# FRR CLI preprocessor +# +# Copyright (C) 2017 David Lamparter for NetDEF, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import _clippy +from _clippy import parse, Graph, GraphNode + +def graph_iterate(graph): + '''iterator yielding all nodes of a graph + + nodes arrive in input/definition order, graph circles are avoided. + ''' + + queue = [(graph.first(), frozenset(), 0)] + while len(queue) > 0: + node, stop, depth = queue.pop(0) + yield node, depth + + join = node.join() + if join is not None: + queue.insert(0, (join, stop.union(frozenset([node])), depth)) + join = frozenset([join]) + + stop = join or stop + nnext = node.next() + for n in reversed(nnext): + if n not in stop and n is not node: + queue.insert(0, (n, stop, depth + 1)) + +def dump(graph): + '''print out clippy.Graph''' + + for i, depth in graph_iterate(graph): + print('\t%s%s %r' % (' ' * (depth * 2), i.type, i.text)) + +def wrdiff(filename, buf): + '''write buffer to file if contents changed''' + + expl = '' + if hasattr(buf, 'getvalue'): + buf = buf.getvalue() + old = None + try: old = open(filename, 'r').read() + except: pass + if old == buf: + # sys.stderr.write('%s unchanged, not written\n' % (filename)) + return + with open('.new.' + filename, 'w') as out: + out.write(buf) + os.rename('.new.' + filename, filename) |
