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