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