]> git.proxmox.com Git - mirror_frr.git/blob - python/clidef.py
Merge pull request #12798 from donaldsharp/rib_match_multicast
[mirror_frr.git] / python / clidef.py
1 # SPDX-License-Identifier: GPL-2.0-or-later
2 # FRR CLI preprocessor (DEFPY)
3 #
4 # Copyright (C) 2017 David Lamparter for NetDEF, Inc.
5
6 import clippy, traceback, sys, os
7 from collections import OrderedDict
8 from functools import reduce
9 from pprint import pprint
10 from string import Template
11 from io import StringIO
12
13 # the various handlers generate output C code for a particular type of
14 # CLI token, choosing the most useful output C type.
15
16
17 class RenderHandler(object):
18 def __init__(self, token):
19 pass
20
21 def combine(self, other):
22 if type(self) == type(other):
23 return other
24 return StringHandler(None)
25
26 deref = ""
27 drop_str = False
28 canfail = True
29 canassert = False
30
31
32 class StringHandler(RenderHandler):
33 argtype = "const char *"
34 decl = Template("const char *$varname = NULL;")
35 code = Template(
36 "$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;"
37 )
38 drop_str = True
39 canfail = False
40 canassert = True
41
42
43 class LongHandler(RenderHandler):
44 argtype = "long"
45 decl = Template("long $varname = 0;")
46 code = Template(
47 """\
48 char *_end;
49 $varname = strtol(argv[_i]->arg, &_end, 10);
50 _fail = (_end == argv[_i]->arg) || (*_end != '\\0');"""
51 )
52
53
54 class AsDotHandler(RenderHandler):
55 argtype = "as_t"
56 decl = Template("as_t $varname = 0;")
57 code = Template("_fail = !asn_str2asn(argv[_i]->arg, &$varname);")
58
59
60 # A.B.C.D/M (prefix_ipv4) and
61 # X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a
62 # struct prefix:
63
64
65 class PrefixBase(RenderHandler):
66 def combine(self, other):
67 if type(self) == type(other):
68 return other
69 if isinstance(other, PrefixBase):
70 return PrefixGenHandler(None)
71 return StringHandler(None)
72
73 deref = "&"
74
75
76 class Prefix4Handler(PrefixBase):
77 argtype = "const struct prefix_ipv4 *"
78 decl = Template("struct prefix_ipv4 $varname = { };")
79 code = Template("_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);")
80
81
82 class Prefix6Handler(PrefixBase):
83 argtype = "const struct prefix_ipv6 *"
84 decl = Template("struct prefix_ipv6 $varname = { };")
85 code = Template("_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);")
86
87
88 class PrefixEthHandler(PrefixBase):
89 argtype = "struct prefix_eth *"
90 decl = Template("struct prefix_eth $varname = { };")
91 code = Template("_fail = !str2prefix_eth(argv[_i]->arg, &$varname);")
92
93
94 class PrefixGenHandler(PrefixBase):
95 argtype = "const struct prefix *"
96 decl = Template("struct prefix $varname = { };")
97 code = Template("_fail = !str2prefix(argv[_i]->arg, &$varname);")
98
99
100 # same for IP addresses. result is union sockunion.
101 class IPBase(RenderHandler):
102 def combine(self, other):
103 if type(self) == type(other):
104 return other
105 if type(other) in [IP4Handler, IP6Handler, IPGenHandler]:
106 return IPGenHandler(None)
107 return StringHandler(None)
108
109
110 class IP4Handler(IPBase):
111 argtype = "struct in_addr"
112 decl = Template("struct in_addr $varname = { INADDR_ANY };")
113 code = Template("_fail = !inet_aton(argv[_i]->arg, &$varname);")
114
115
116 class IP6Handler(IPBase):
117 argtype = "struct in6_addr"
118 decl = Template("struct in6_addr $varname = {};")
119 code = Template("_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);")
120
121
122 class IPGenHandler(IPBase):
123 argtype = "const union sockunion *"
124 decl = Template(
125 """union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;"""
126 )
127 code = Template(
128 """\
129 if (argv[_i]->text[0] == 'X') {
130 s__$varname.sa.sa_family = AF_INET6;
131 _fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr);
132 $varname = &s__$varname;
133 } else {
134 s__$varname.sa.sa_family = AF_INET;
135 _fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr);
136 $varname = &s__$varname;
137 }"""
138 )
139 canassert = True
140
141
142 def mix_handlers(handlers):
143 def combine(a, b):
144 if a is None:
145 return b
146 return a.combine(b)
147
148 return reduce(combine, handlers, None)
149
150
151 handlers = {
152 "WORD_TKN": StringHandler,
153 "VARIABLE_TKN": StringHandler,
154 "RANGE_TKN": LongHandler,
155 "IPV4_TKN": IP4Handler,
156 "IPV4_PREFIX_TKN": Prefix4Handler,
157 "IPV6_TKN": IP6Handler,
158 "IPV6_PREFIX_TKN": Prefix6Handler,
159 "MAC_TKN": PrefixEthHandler,
160 "MAC_PREFIX_TKN": PrefixEthHandler,
161 "ASNUM_TKN": AsDotHandler,
162 }
163
164 # core template invoked for each occurence of DEFPY.
165 #
166 # the "#if $..." bits are there to keep this template unified into one
167 # common form, without requiring a more advanced template engine (e.g.
168 # jinja2)
169 templ = Template(
170 """$cond_begin/* $fnname => "$cmddef" */
171 DEFUN_CMD_FUNC_DECL($fnname)
172 #define funcdecl_$fnname static int ${fnname}_magic(\\
173 const struct cmd_element *self __attribute__ ((unused)),\\
174 struct vty *vty __attribute__ ((unused)),\\
175 int argc __attribute__ ((unused)),\\
176 struct cmd_token *argv[] __attribute__ ((unused))$argdefs)
177 funcdecl_$fnname;
178 DEFUN_CMD_FUNC_TEXT($fnname)
179 {
180 #if $nonempty /* anything to parse? */
181 int _i;
182 #if $canfail /* anything that can fail? */
183 unsigned _fail = 0, _failcnt = 0;
184 #endif
185 $argdecls
186 for (_i = 0; _i < argc; _i++) {
187 if (!argv[_i]->varname)
188 continue;
189 #if $canfail /* anything that can fail? */
190 _fail = 0;
191 #endif
192 $argblocks
193 #if $canfail /* anything that can fail? */
194 if (_fail)
195 vty_out (vty, "%% invalid input for %s: %s\\n",
196 argv[_i]->varname, argv[_i]->arg);
197 _failcnt += _fail;
198 #endif
199 }
200 #if $canfail /* anything that can fail? */
201 if (_failcnt)
202 return CMD_WARNING;
203 #endif
204 #endif
205 $argassert
206 return ${fnname}_magic(self, vty, argc, argv$arglist);
207 }
208 $cond_end
209 """
210 )
211
212 # invoked for each named parameter
213 argblock = Template(
214 """
215 if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock
216 $code
217 }"""
218 )
219
220
221 def get_always_args(token, always_args, args=[], stack=[]):
222 if token in stack:
223 return
224 if token.type == "END_TKN":
225 for arg in list(always_args):
226 if arg not in args:
227 always_args.remove(arg)
228 return
229
230 stack = stack + [token]
231 if token.type in handlers and token.varname is not None:
232 args = args + [token.varname]
233 for nexttkn in token.next():
234 get_always_args(nexttkn, always_args, args, stack)
235
236
237 class Macros(dict):
238 def __init__(self):
239 super().__init__()
240 self._loc = {}
241
242 def load(self, filename):
243 filedata = clippy.parse(filename)
244 for entry in filedata["data"]:
245 if entry["type"] != "PREPROC":
246 continue
247 self.load_preproc(filename, entry)
248
249 def setup(self, key, val, where="built-in"):
250 self[key] = val
251 self._loc[key] = (where, 0)
252
253 def load_preproc(self, filename, entry):
254 ppdir = entry["line"].lstrip().split(None, 1)
255 if ppdir[0] != "define" or len(ppdir) != 2:
256 return
257 ppdef = ppdir[1].split(None, 1)
258 name = ppdef[0]
259 if "(" in name:
260 return
261 val = ppdef[1] if len(ppdef) == 2 else ""
262
263 val = val.strip(" \t\n\\")
264 if self.get(name, val) != val:
265 sys.stderr.write(
266 "%s:%d: warning: macro %s redefined!\n"
267 % (
268 filename,
269 entry["lineno"],
270 name,
271 )
272 )
273 sys.stderr.write(
274 "%s:%d: note: previously defined here\n"
275 % (
276 self._loc[name][0],
277 self._loc[name][1],
278 )
279 )
280 else:
281 self[name] = val
282 self._loc[name] = (filename, entry["lineno"])
283
284
285 def process_file(fn, ofd, dumpfd, all_defun, macros):
286 errors = 0
287 filedata = clippy.parse(fn)
288
289 cond_stack = []
290
291 for entry in filedata["data"]:
292 if entry["type"] == "PREPROC":
293 line = entry["line"].lstrip()
294 tokens = line.split(maxsplit=1)
295 line = "#" + line + "\n"
296
297 if not tokens:
298 continue
299
300 if tokens[0] in ["if", "ifdef", "ifndef"]:
301 cond_stack.append(line)
302 elif tokens[0] in ["elif", "else"]:
303 prev_line = cond_stack.pop(-1)
304 cond_stack.append(prev_line + line)
305 elif tokens[0] in ["endif"]:
306 cond_stack.pop(-1)
307 elif tokens[0] in ["define"]:
308 if not cond_stack:
309 macros.load_preproc(fn, entry)
310 elif len(cond_stack) == 1 and cond_stack[0] == "#ifdef CLIPPY\n":
311 macros.load_preproc(fn, entry)
312 continue
313 if entry["type"].startswith("DEFPY") or (
314 all_defun and entry["type"].startswith("DEFUN")
315 ):
316 if len(entry["args"][0]) != 1:
317 sys.stderr.write(
318 "%s:%d: DEFPY function name not parseable (%r)\n"
319 % (fn, entry["lineno"], entry["args"][0])
320 )
321 errors += 1
322 continue
323
324 cmddef = entry["args"][2]
325 cmddefx = []
326 for i in cmddef:
327 while i in macros:
328 i = macros[i]
329 if i.startswith('"') and i.endswith('"'):
330 cmddefx.append(i[1:-1])
331 continue
332
333 sys.stderr.write(
334 "%s:%d: DEFPY command string not parseable (%r)\n"
335 % (fn, entry["lineno"], cmddef)
336 )
337 errors += 1
338 cmddefx = None
339 break
340 if cmddefx is None:
341 continue
342 cmddef = "".join([i for i in cmddefx])
343
344 graph = clippy.Graph(cmddef)
345 args = OrderedDict()
346 always_args = set()
347 for token, depth in clippy.graph_iterate(graph):
348 if token.type not in handlers:
349 continue
350 if token.varname is None:
351 continue
352 arg = args.setdefault(token.varname, [])
353 arg.append(handlers[token.type](token))
354 always_args.add(token.varname)
355
356 get_always_args(graph.first(), always_args)
357
358 # print('-' * 76)
359 # pprint(entry)
360 # clippy.dump(graph)
361 # pprint(args)
362
363 params = {"cmddef": cmddef, "fnname": entry["args"][0][0]}
364 argdefs = []
365 argdecls = []
366 arglist = []
367 argblocks = []
368 argassert = []
369 doc = []
370 canfail = 0
371
372 def do_add(handler, basename, varname, attr=""):
373 argdefs.append(",\\\n\t%s %s%s" % (handler.argtype, varname, attr))
374 argdecls.append(
375 "\t%s\n"
376 % (
377 handler.decl.substitute({"varname": varname}).replace(
378 "\n", "\n\t"
379 )
380 )
381 )
382 arglist.append(", %s%s" % (handler.deref, varname))
383 if basename in always_args and handler.canassert:
384 argassert.append(
385 """\tif (!%s) {
386 \t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s");
387 \t\treturn CMD_WARNING;
388 \t}\n"""
389 % (varname, varname)
390 )
391 if attr == "":
392 at = handler.argtype
393 if not at.startswith("const "):
394 at = ". . . " + at
395 doc.append(
396 "\t%-26s %s %s"
397 % (at, "alw" if basename in always_args else "opt", varname)
398 )
399
400 for varname in args.keys():
401 handler = mix_handlers(args[varname])
402 # print(varname, handler)
403 if handler is None:
404 continue
405 do_add(handler, varname, varname)
406 code = handler.code.substitute({"varname": varname}).replace(
407 "\n", "\n\t\t\t"
408 )
409 if handler.canfail:
410 canfail = 1
411 strblock = ""
412 if not handler.drop_str:
413 do_add(
414 StringHandler(None),
415 varname,
416 "%s_str" % (varname),
417 " __attribute__ ((unused))",
418 )
419 strblock = "\n\t\t\t%s_str = argv[_i]->arg;" % (varname)
420 argblocks.append(
421 argblock.substitute(
422 {"varname": varname, "strblock": strblock, "code": code}
423 )
424 )
425
426 if dumpfd is not None:
427 if len(arglist) > 0:
428 dumpfd.write('"%s":\n%s\n\n' % (cmddef, "\n".join(doc)))
429 else:
430 dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef))
431
432 params["cond_begin"] = "".join(cond_stack)
433 params["cond_end"] = "".join(["#endif\n"] * len(cond_stack))
434 params["argdefs"] = "".join(argdefs)
435 params["argdecls"] = "".join(argdecls)
436 params["arglist"] = "".join(arglist)
437 params["argblocks"] = "".join(argblocks)
438 params["canfail"] = canfail
439 params["nonempty"] = len(argblocks)
440 params["argassert"] = "".join(argassert)
441 ofd.write(templ.substitute(params))
442
443 return errors
444
445
446 if __name__ == "__main__":
447 import argparse
448
449 argp = argparse.ArgumentParser(description="FRR CLI preprocessor in Python")
450 argp.add_argument(
451 "--all-defun",
452 action="store_const",
453 const=True,
454 help="process DEFUN() statements in addition to DEFPY()",
455 )
456 argp.add_argument(
457 "--show",
458 action="store_const",
459 const=True,
460 help="print out list of arguments and types for each definition",
461 )
462 argp.add_argument("-o", type=str, metavar="OUTFILE", help="output C file name")
463 argp.add_argument("cfile", type=str)
464 args = argp.parse_args()
465
466 dumpfd = None
467 if args.o is not None:
468 ofd = StringIO()
469 if args.show:
470 dumpfd = sys.stdout
471 else:
472 ofd = sys.stdout
473 if args.show:
474 dumpfd = sys.stderr
475
476 basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
477
478 macros = Macros()
479 macros.load("lib/route_types.h")
480 macros.load(os.path.join(basepath, "lib/command.h"))
481 macros.load(os.path.join(basepath, "bgpd/bgp_vty.h"))
482 # sigh :(
483 macros.setup("PROTO_REDIST_STR", "FRR_REDIST_STR_ISISD")
484 macros.setup("PROTO_IP_REDIST_STR", "FRR_IP_REDIST_STR_ISISD")
485 macros.setup("PROTO_IP6_REDIST_STR", "FRR_IP6_REDIST_STR_ISISD")
486
487 errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros)
488 if errors != 0:
489 sys.exit(1)
490
491 if args.o is not None:
492 clippy.wrdiff(
493 args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable]
494 )