]> git.proxmox.com Git - mirror_frr.git/commitdiff
lib: clippy the assistant
authorDavid Lamparter <equinox@opensourcerouting.org>
Fri, 11 Nov 2016 16:10:02 +0000 (17:10 +0100)
committerDavid Lamparter <equinox@opensourcerouting.org>
Wed, 14 Jun 2017 17:29:23 +0000 (19:29 +0200)
Wraps the command parsing code for Python, so we can use it to do fancy
preprocessing and replace extract.pl.

Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
configure.ac
lib/Makefile.am
lib/clippy.c [new file with mode: 0644]
lib/clippy.h [new file with mode: 0644]
lib/command_py.c [new file with mode: 0644]
lib/defun_lex.l [new file with mode: 0644]

index 29bff56a5e676690022a97384308cfa9cc7ad427..ca8eae9057a25de6fa09eb33a3234af273a71c69 100755 (executable)
@@ -23,7 +23,36 @@ dnl Get hostname and other information.
 dnl -----------------------------------
 AC_CANONICAL_BUILD()
 AC_CANONICAL_HOST()
-AC_CANONICAL_TARGET()
+
+AS_IF([test "$host" != "$build"], [
+  if test "$srcdir" = "."; then
+    AC_MSG_ERROR([cross-compilation is only possible with builddir separate from srcdir.  create a separate directory and run as .../path-to-frr/configure.])
+  fi
+  test -d hosttools || mkdir hosttools
+  abssrc="`cd \"${srcdir}\"; pwd`"
+
+  AC_MSG_NOTICE([...])
+  AC_MSG_NOTICE([... cross-compilation: creating hosttools directory and self-configuring for build platform tools])
+  AC_MSG_NOTICE([... use HOST_CPPFLAGS / HOST_CFLAGS / HOST_LDFLAGS if neccessary])
+  AC_MSG_NOTICE([...])
+
+  ( CPPFLAGS="$HOST_CPPFLAGS"; \
+    CFLAGS="$HOST_CFLAGS"; \
+    LDFLAGS="$HOST_LDFLAGS"; \
+    cd hosttools; "${abssrc}/configure" "--host=$build" "--build=$build"; )
+
+  AC_MSG_NOTICE([...])
+  AC_MSG_NOTICE([... cross-compilation: finished self-configuring for build platform tools])
+  AC_MSG_NOTICE([...])
+
+  build_clippy="false"
+  CLIPPYDIR="hosttools/lib"
+], [
+  build_clippy="true"
+  CLIPPYDIR="lib"
+])
+AC_SUBST(CLIPPYDIR)
+AM_CONDITIONAL([BUILD_CLIPPY], [$build_clippy])
 
 # Disable portability warnings -- our automake code (in particular
 # common.am) uses some constructs specific to gmake.
@@ -431,6 +460,77 @@ if test "x${enable_dev_build}" = "xyes"; then
 fi
 AM_CONDITIONAL([DEV_BUILD], [test "x$enable_dev_build" = "xyes"])
 
+#
+# Python for clippy
+#
+AS_IF([test "$host" = "$build"], [
+  PYTHONCONFIG=""
+
+  # ordering:
+  # 1.  try python3, but respect the user's preference on which minor ver
+  # 2.  try python, which might be py3 or py2 again on the user's preference
+  # 3.  try python2 (can really only be 2.7 but eh)
+  # 4.  try 3.5 > 3.4 > 3.3 > 3.2 > 2.7 through pkg-config (no user pref)
+  #
+  # (AX_PYTHON_DEVEL has no clue about py3 vs py2)
+  # (AX_PYTHON does not do what we need)
+
+  AC_CHECK_TOOLS([PYTHONCONFIG], [python3-config python-config python2-config])
+  if test -n "$PYTHONCONFIG"; then
+    PYTHON_CFLAGS="`\"${PYTHONCONFIG}\" --includes`"
+    PYTHON_LIBS="`\"${PYTHONCONFIG}\" --libs`"
+
+    AC_MSG_CHECKING([whether we found a working Python version])
+    AC_LINK_IFELSE_FLAGS([$PYTHON_CFLAGS], [$PYTHON_LIBS], [AC_LANG_PROGRAM([
+#include <Python.h>
+#if PY_VERSION_HEX < 0x02070000
+#error python too old
+#endif
+int main(void);
+],
+[
+{
+  Py_Initialize();
+  return 0;
+}
+])], [
+      PYTHONCONFIG=""
+      unset PYTHON_LIBS
+      unset PYTHON_CFLAGS
+    ])
+  fi
+
+  if test -z "$PYTHONCONFIG"; then
+    PKG_CHECK_MODULES([PYTHON], python-3.5, [], [
+      PKG_CHECK_MODULES([PYTHON], python-3.4, [], [
+        PKG_CHECK_MODULES([PYTHON], python-3.3, [], [
+          PKG_CHECK_MODULES([PYTHON], python-3.2, [], [
+            PKG_CHECK_MODULES([PYTHON], python-2.7, [], [
+              AC_MSG_FAILURE([could not find python-config or pkg-config python, please install Python development files from libpython-dev or similar])
+              ])])])])])
+
+
+    AC_MSG_CHECKING([whether we found a working Python version])
+    AC_LINK_IFELSE_FLAGS([$PYTHON_CFLAGS], [$PYTHON_LIBS], [AC_LANG_PROGRAM([
+#include <Python.h>
+#if PY_VERSION_HEX < 0x02070000
+#error python too old
+#endif
+int main(void);
+],
+[
+{
+  Py_Initialize();
+  return 0;
+}
+])], [
+      AC_MSG_FAILURE([could not find python-config or pkg-config python, please install Python development files from libpython-dev or similar])
+    ])
+  fi
+])
+AC_SUBST(PYTHON_CFLAGS)
+AC_SUBST(PYTHON_LIBS)
+
 #
 # Logic for protobuf support.
 #
index a1b78d3c4dae8e985a6bbddc10cb6f882e4e6418..25d7d6d95093370c16458f4196618fd03cd6d461 100644 (file)
@@ -9,6 +9,7 @@ command_lex.h: command_lex.c
        @if test ! -f $@; then rm -f command_lex.c; else :; fi
        @if test ! -f $@; then $(MAKE) $(AM_MAKEFLAGS) command_lex.c; else :; fi
 command_parse.lo: command_lex.h
+clippy-command_parse.$(OBJEXT): command_lex.h
 
 lib_LTLIBRARIES = libfrr.la
 libfrr_la_LDFLAGS = -version-info 0:0:0 
@@ -85,13 +86,34 @@ pkginclude_HEADERS = \
 
 noinst_HEADERS = \
        plist_int.h \
-       log_int.h
+       log_int.h \
+       clippy.h \
+       # end
 
 noinst_PROGRAMS = grammar_sandbox
+if BUILD_CLIPPY
+noinst_PROGRAMS += clippy
+endif
 
 grammar_sandbox_SOURCES = grammar_sandbox_main.c
 grammar_sandbox_LDADD = libfrr.la
 
+clippy_SOURCES = \
+       defun_lex.l \
+       command_parse.y \
+       command_lex.l \
+       command_graph.c \
+       command_py.c \
+       memory.c \
+       graph.c \
+       vector.c \
+       clippy.c \
+       # end
+clippy_CPPFLAGS = -D_GNU_SOURCE
+clippy_CFLAGS   = $(PYTHON_CFLAGS)
+clippy_LDADD    = $(PYTHON_LIBS)
+clippy-command_graph.$(OBJEXT): route_types.h
+
 EXTRA_DIST = \
        queue.h \
        command_lex.h \
diff --git a/lib/clippy.c b/lib/clippy.c
new file mode 100644 (file)
index 0000000..26ef246
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * clippy (CLI preparator in python) main executable
+ * Copyright (C) 2016-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
+ */
+
+#include "config.h"
+#include <Python.h>
+#include <string.h>
+#include <stdlib.h>
+#include <wchar.h>
+#include "getopt.h"
+
+#include "command_graph.h"
+#include "clippy.h"
+
+#if PY_MAJOR_VERSION >= 3
+#define pychar wchar_t
+static wchar_t *wconv(const char *s)
+{
+       size_t outlen = mbstowcs(NULL, s, 0);
+       wchar_t *out = malloc((outlen + 1) * sizeof(wchar_t));
+       mbstowcs(out, s, outlen + 1);
+       out[outlen] = 0;
+       return out;
+}
+#else
+#define pychar char
+#define wconv(x) x
+#endif
+
+int main(int argc, char **argv)
+{
+       pychar **wargv;
+
+#if PY_VERSION_HEX >= 0x03040000 /* 3.4 */
+       Py_SetStandardStreamEncoding("UTF-8", NULL);
+#endif
+       Py_SetProgramName(wconv(argv[0]));
+       PyImport_AppendInittab("_clippy", command_py_init);
+
+       Py_Initialize();
+
+       wargv = malloc(argc * sizeof(pychar *));
+       for (int i = 1; i < argc; i++)
+               wargv[i - 1] = wconv(argv[i]);
+       PySys_SetArgv(argc - 1, wargv);
+
+       const char *pyfile = argc > 1 ? argv[1] : NULL;
+       FILE *fp;
+       if (pyfile) {
+               fp = fopen(pyfile, "r");
+               if (!fp) {
+                       fprintf(stderr, "%s: %s\n", pyfile, strerror(errno));
+                       return 1;
+               }
+       } else {
+               fp = stdin;
+               char *ver = strdup(Py_GetVersion());
+               char *cr = strchr(ver, '\n');
+               if (cr)
+                       *cr = ' ';
+               fprintf(stderr, "clippy interactive shell\n(Python %s)\n", ver);
+               free(ver);
+               PyRun_SimpleString("import rlcompleter, readline\n"
+                               "readline.parse_and_bind('tab: complete')");
+       }
+
+       if (PyRun_AnyFile(fp, pyfile)) {
+               if (PyErr_Occurred())
+                       PyErr_Print();
+               else
+                       printf("unknown python failure (?)\n");
+               return 1;
+       }
+        Py_Finalize();
+
+#if PY_MAJOR_VERSION >= 3
+       for (int i = 1; i < argc; i++)
+               free(wargv[i - 1]);
+#endif
+       free(wargv);
+       return 0;
+}
+
+/* and now for the ugly part... provide simplified logging functions so we
+ * don't need to link libzebra (which would be a circular build dep) */
+
+#ifdef __ASSERT_FUNCTION
+#undef __ASSERT_FUNCTION
+#endif
+
+#include "log.h"
+#include "zassert.h"
+
+#define ZLOG_FUNC(FUNCNAME) \
+void FUNCNAME(const char *format, ...) \
+{ \
+  va_list args; \
+  va_start(args, format); \
+  vfprintf (stderr, format, args); \
+  fputs ("\n", stderr); \
+  va_end(args); \
+}
+
+ZLOG_FUNC(zlog_err)
+ZLOG_FUNC(zlog_warn)
+ZLOG_FUNC(zlog_info)
+ZLOG_FUNC(zlog_notice)
+ZLOG_FUNC(zlog_debug)
+
+void
+_zlog_assert_failed (const char *assertion, const char *file,
+                    unsigned int line, const char *function)
+{
+       fprintf(stderr, "Assertion `%s' failed in file %s, line %u, function %s",
+               assertion, file, line, (function ? function : "?"));
+       abort();
+}
+
+void memory_oom (size_t size, const char *name)
+{
+       abort();
+}
diff --git a/lib/clippy.h b/lib/clippy.h
new file mode 100644 (file)
index 0000000..8df98cb
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * clippy (CLI preparator in python)
+ * Copyright (C) 2016-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
+ */
+
+#ifndef _FRR_CLIPPY_H
+#define _FRR_CLIPPY_H
+
+#include <Python.h>
+
+extern PyObject *clippy_parse(PyObject *self, PyObject *args);
+extern PyMODINIT_FUNC command_py_init(void);
+
+#endif /* _FRR_CLIPPY_H */
diff --git a/lib/command_py.c b/lib/command_py.c
new file mode 100644 (file)
index 0000000..6621b0f
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+ * clippy (CLI preparator in python) wrapper for FRR command_graph
+ * Copyright (C) 2016-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
+ */
+
+/* note: this wrapper is intended to be used as build-time helper.  while
+ * it should be generally correct and proper, there may be the occasional
+ * memory leak or SEGV for things that haven't been well-tested.
+ */
+
+#include <Python.h>
+#include "structmember.h"
+#include <string.h>
+#include <stdlib.h>
+
+#include "command_graph.h"
+#include "clippy.h"
+
+struct wrap_graph;
+static PyObject *graph_to_pyobj(struct wrap_graph *graph, struct graph_node *gn);
+
+/*
+ * nodes are wrapped as follows:
+ *  - instances can only be acquired from a graph
+ *  - the same node will return the same wrapper object (they're buffered
+ *    through "idx")
+ *  - a reference is held onto the graph
+ *  - fields are copied for easy access with PyMemberDef
+ */
+struct wrap_graph_node {
+       PyObject_HEAD
+
+       bool allowrepeat;
+       const char *type;
+
+       bool deprecated;
+       bool hidden;
+       const char *text;
+       const char *desc;
+       const char *varname;
+       long long min, max;
+
+       struct graph_node *node;
+       struct wrap_graph *wgraph;
+       size_t idx;
+};
+
+/*
+ * graphs are wrapped as follows:
+ *  - they can only be created by parsing a definition string
+ *  - there's a table here for the wrapped nodes (nodewrappers), indexed
+ *    by "idx" (corresponds to node's position in graph's table of nodes)
+ *  - graphs do NOT hold references to nodes (would be circular)
+ */
+struct wrap_graph {
+       PyObject_HEAD
+
+       char *definition;
+       struct graph *graph;
+       struct wrap_graph_node **nodewrappers;
+};
+
+static PyObject *refuse_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+       PyErr_SetString(PyExc_ValueError, "cannot create instances of this type");
+       return NULL;
+}
+
+#define member(name, type) {(char *)#name, type, offsetof(struct wrap_graph_node, name), READONLY, \
+       (char *)#name " (" #type ")"}
+static PyMemberDef members_graph_node[] = {
+       member(allowrepeat, T_BOOL),
+       member(type, T_STRING),
+       member(deprecated, T_BOOL),
+       member(hidden, T_BOOL),
+       member(text, T_STRING),
+       member(desc, T_STRING),
+       member(min, T_LONGLONG),
+       member(max, T_LONGLONG),
+       member(varname, T_STRING),
+       {},
+};
+#undef member
+
+/*
+ * node.next() -- returns list of all "next" nodes.
+ * this will include circles if the graph has them.
+ */
+static PyObject *graph_node_next(PyObject *self, PyObject *args)
+{
+       struct wrap_graph_node *wrap = (struct wrap_graph_node *)self;
+       PyObject *pylist;
+
+       if (wrap->node->data
+               && ((struct cmd_token *)wrap->node->data)->type == END_TKN)
+               return PyList_New(0);
+       pylist = PyList_New(vector_active(wrap->node->to));
+       for (size_t i = 0; i < vector_active(wrap->node->to); i++) {
+               struct graph_node *gn = vector_slot(wrap->node->to, i);
+               PyList_SetItem(pylist, i, graph_to_pyobj(wrap->wgraph, gn));
+       }
+       return pylist;
+};
+
+/*
+ * node.join() -- return FORK's JOIN node or None
+ */
+static PyObject *graph_node_join(PyObject *self, PyObject *args)
+{
+       struct wrap_graph_node *wrap = (struct wrap_graph_node *)self;
+
+       if (!wrap->node->data
+               || ((struct cmd_token *)wrap->node->data)->type == END_TKN)
+               Py_RETURN_NONE;
+
+       struct cmd_token *tok = wrap->node->data;
+       if (tok->type != FORK_TKN)
+               Py_RETURN_NONE;
+
+       return graph_to_pyobj(wrap->wgraph, tok->forkjoin);
+};
+
+static PyMethodDef methods_graph_node[] = {
+       {"next", graph_node_next, METH_NOARGS, "outbound graph edge list"},
+       {"join", graph_node_join, METH_NOARGS, "outbound join node"},
+       {}
+};
+
+static void graph_node_wrap_free(void *arg)
+{
+       struct wrap_graph_node *wrap = arg;
+       wrap->wgraph->nodewrappers[wrap->idx] = NULL;
+       Py_DECREF(wrap->wgraph);
+}
+
+static PyTypeObject typeobj_graph_node = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name        = "_clippy.GraphNode",
+       .tp_basicsize   = sizeof(struct wrap_graph_node),
+       .tp_flags       = Py_TPFLAGS_DEFAULT,
+       .tp_doc         = "struct graph_node *",
+       .tp_new         = refuse_new,
+       .tp_free        = graph_node_wrap_free,
+       .tp_members     = members_graph_node,
+       .tp_methods     = methods_graph_node,
+};
+
+static PyObject *graph_to_pyobj(struct wrap_graph *wgraph, struct graph_node *gn)
+{
+       struct wrap_graph_node *wrap;
+       size_t i;
+
+       for (i = 0; i < vector_active(wgraph->graph->nodes); i++)
+               if (vector_slot(wgraph->graph->nodes, i) == gn)
+                       break;
+       if (i == vector_active(wgraph->graph->nodes)) {
+               PyErr_SetString(PyExc_ValueError, "cannot find node in graph");
+               return NULL;
+       }
+       if (wgraph->nodewrappers[i]) {
+               PyObject *obj = (PyObject *)wgraph->nodewrappers[i];
+               Py_INCREF(obj);
+               return obj;
+       }
+
+       wrap = (struct wrap_graph_node *)typeobj_graph_node.tp_alloc(&typeobj_graph_node, 0);
+       if (!wrap)
+               return NULL;
+       wgraph->nodewrappers[i] = wrap;
+       Py_INCREF(wgraph);
+
+       wrap->idx = i;
+       wrap->wgraph = wgraph;
+       wrap->node = gn;
+       wrap->type = "NULL";
+       wrap->allowrepeat = false;
+       if (gn->data) {
+               struct cmd_token *tok = gn->data;
+               switch (tok->type) {
+#define item(x) case x: wrap->type = #x; break;
+               item(WORD_TKN)          // words
+               item(VARIABLE_TKN)      // almost anything
+               item(RANGE_TKN)         // integer range
+               item(IPV4_TKN)          // IPV4 addresses
+               item(IPV4_PREFIX_TKN)   // IPV4 network prefixes
+               item(IPV6_TKN)          // IPV6 prefixes
+               item(IPV6_PREFIX_TKN)   // IPV6 network prefixes
+
+               /* plumbing types */
+               item(FORK_TKN)
+               item(JOIN_TKN)
+               item(START_TKN)
+               item(END_TKN)
+               default:
+                       wrap->type = "???";
+               }
+
+               wrap->deprecated = (tok->attr == CMD_ATTR_DEPRECATED);
+               wrap->hidden = (tok->attr == CMD_ATTR_HIDDEN);
+               wrap->text = tok->text;
+               wrap->desc = tok->desc;
+               wrap->varname = tok->varname;
+               wrap->min = tok->min;
+               wrap->max = tok->max;
+               wrap->allowrepeat = tok->allowrepeat;
+       }
+
+       return (PyObject *)wrap;
+}
+
+#define member(name, type) {(char *)#name, type, offsetof(struct wrap_graph, name), READONLY, \
+       (char *)#name " (" #type ")"}
+static PyMemberDef members_graph[] = {
+       member(definition, T_STRING),
+       {},
+};
+#undef member
+
+/* graph.first() - root node */
+static PyObject *graph_first(PyObject *self, PyObject *args)
+{
+       struct wrap_graph *gwrap = (struct wrap_graph *)self;
+       struct graph_node *gn = vector_slot(gwrap->graph->nodes, 0);
+       return graph_to_pyobj(gwrap, gn);
+};
+
+static PyMethodDef methods_graph[] = {
+       {"first", graph_first, METH_NOARGS, "first graph node"},
+       {}
+};
+
+static PyObject *graph_parse(PyTypeObject *type, PyObject *args, PyObject *kwds);
+
+static void graph_wrap_free(void *arg)
+{
+       struct wrap_graph *wgraph = arg;
+       free(wgraph->nodewrappers);
+       free(wgraph->definition);
+}
+
+static PyTypeObject typeobj_graph = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name        = "_clippy.Graph",
+       .tp_basicsize   = sizeof(struct wrap_graph),
+       .tp_flags       = Py_TPFLAGS_DEFAULT,
+       .tp_doc         = "struct graph *",
+       .tp_new         = graph_parse,
+       .tp_free        = graph_wrap_free,
+       .tp_members     = members_graph,
+       .tp_methods     = methods_graph,
+};
+
+/* top call / entrypoint for python code */
+static PyObject *graph_parse(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+       const char *def, *doc = NULL;
+       struct wrap_graph *gwrap;
+       static const char *kwnames[] = { "cmddef", "doc", NULL };
+
+       gwrap = (struct wrap_graph *)typeobj_graph.tp_alloc(&typeobj_graph, 0);
+       if (!gwrap)
+               return NULL;
+
+       if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|s", (char **)kwnames, &def, &doc))
+               return NULL;
+
+       struct graph *graph = graph_new ();
+       struct cmd_token *token = cmd_token_new (START_TKN, 0, NULL, NULL);
+       graph_new_node (graph, token, (void (*)(void *)) &cmd_token_del);
+
+       struct cmd_element cmd = { .string = def, .doc = doc };
+       cmd_graph_parse (graph, &cmd);
+       cmd_graph_names (graph);
+
+       gwrap->graph = graph;
+       gwrap->definition = strdup(def);
+       gwrap->nodewrappers = calloc(vector_active(graph->nodes),
+                       sizeof (gwrap->nodewrappers[0]));
+       return (PyObject *)gwrap;
+}
+
+static PyMethodDef clippy_methods[] = {
+       {"parse", clippy_parse, METH_VARARGS, "Parse a C file"},
+       {NULL, NULL, 0, NULL}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef pymoddef_clippy = {
+       PyModuleDef_HEAD_INIT,
+       "_clippy",
+       NULL, /* docstring */
+       -1,
+       clippy_methods,
+};
+#define modcreate() PyModule_Create(&pymoddef_clippy)
+#define initret(val) return val;
+#else
+#define modcreate() Py_InitModule("_clippy", clippy_methods)
+#define initret(val) do { \
+       if (!val) Py_FatalError("initialization failure"); \
+       return; } while (0)
+#endif
+
+PyMODINIT_FUNC command_py_init(void)
+{
+       PyObject* pymod;
+
+       if (PyType_Ready(&typeobj_graph_node) < 0)
+               initret(NULL);
+       if (PyType_Ready(&typeobj_graph) < 0)
+               initret(NULL);
+
+       pymod = modcreate();
+       if (!pymod)
+               initret(NULL);
+
+       Py_INCREF(&typeobj_graph_node);
+       PyModule_AddObject(pymod, "GraphNode", (PyObject *)&typeobj_graph_node);
+       Py_INCREF(&typeobj_graph);
+       PyModule_AddObject(pymod, "Graph", (PyObject *)&typeobj_graph);
+       initret(pymod);
+}
diff --git a/lib/defun_lex.l b/lib/defun_lex.l
new file mode 100644 (file)
index 0000000..8aa37a6
--- /dev/null
@@ -0,0 +1,265 @@
+%{
+/*
+ * clippy (CLI preparator in python) C pseudo-lexer
+ * Copyright (C) 2016-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
+ */
+
+/* This is just enough of a lexer to make rough sense of a C source file.
+ * It handles C preprocessor directives, strings, and looks for FRR-specific
+ * idioms (aka DEFUN).
+ *
+ * There is some preliminary support for documentation comments for DEFUNs.
+ * They would look like this (note the ~):  (replace \ by /)
+ *
+ * \*~  documentation for foobar_cmd
+ *  *   parameter does xyz
+ *  *\
+ * DEFUN(foobar_cmd, ...)
+ *
+ * This is intended for user documentation / command reference.  Don't put
+ * code documentation in it.
+ */
+
+/* ignore harmless bug in old versions of flex */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+
+#include "config.h"
+#include <Python.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "command_graph.h"
+#include "clippy.h"
+
+#define ID             258
+#define PREPROC                259
+#define OPERATOR       260
+#define STRING         261
+#define COMMENT                262
+#define SPECIAL                263
+
+#define DEFUNNY                270
+#define INSTALL                271
+#define AUXILIARY      272
+
+int comment_link;
+char string_end;
+
+char *value;
+
+static void extendbuf(char **what, const char *arg)
+{
+       if (!*what)
+               *what = strdup(arg);
+       else {
+               size_t vall = strlen(*what), argl = strlen(arg);
+               *what = realloc(*what, vall + argl + 1);
+               memcpy(*what + vall, arg, argl);
+               (*what)[vall + argl] = '\0';
+       }
+}
+#define extend(x) extendbuf(&value, x)
+
+%}
+
+ID             [A-Za-z0-9_]+
+OPERATOR       [!%&/\[\]{}=?:^|\*.;><~'\\+-]
+SPECIAL                [(),]
+
+%pointer
+%option yylineno
+%option noyywrap
+%option noinput
+%option nounput
+%option outfile="defun_lex.c"
+%option prefix="def_yy"
+%option 8bit
+
+%s linestart
+%x comment
+%x linecomment
+%x preproc
+%x rstring
+%%
+                               BEGIN(linestart);
+
+\n                             BEGIN(linestart);
+
+<INITIAL,linestart,preproc>"/*"        comment_link = YY_START; extend(yytext); BEGIN(comment);
+<comment>[^*\n]*               extend(yytext);
+<comment>"*"+[^*/\n]*          extend(yytext);
+<comment>\n                    extend(yytext);
+<comment>"*"+"/"               extend(yytext); BEGIN(comment_link); return COMMENT;
+
+<INITIAL,linestart,preproc>"//"        comment_link = YY_START; extend(yytext); BEGIN(linecomment);
+<linecomment>[^\n]*            extend(yytext);
+<linecomment>\n                        BEGIN((comment_link == INITIAL) ? linestart : comment_link); return COMMENT;
+
+<linestart>#                   BEGIN(preproc);
+<preproc>\n                    BEGIN(INITIAL); return PREPROC;
+<preproc>[^\n\\]+              extend(yytext);
+<preproc>\\\n                  extend(yytext);
+<preproc>\\+[^\n]              extend(yytext);
+
+[\"\']                         string_end = yytext[0]; extend(yytext); BEGIN(rstring);
+<rstring>[\"\']                        {
+                                       extend(yytext);
+                                       if (yytext[0] == string_end) {
+                                               BEGIN(INITIAL);
+                                               return STRING;
+                                       }
+                               }
+<rstring>\\\n                  /* ignore */
+<rstring>\\.                   extend(yytext);
+<rstring>[^\\\"\']+            extend(yytext);
+
+"DEFUN"                                value = strdup(yytext); return DEFUNNY;
+"DEFUN_NOSH"                   value = strdup(yytext); return DEFUNNY;
+"DEFUN_HIDDEN"                 value = strdup(yytext); return DEFUNNY;
+"DEFPY"                                value = strdup(yytext); return DEFUNNY;
+"ALIAS"                                value = strdup(yytext); return DEFUNNY;
+"ALIAS_HIDDEN"                 value = strdup(yytext); return DEFUNNY;
+"install_element"              value = strdup(yytext); return INSTALL;
+"VTYSH_TARGETS"                        value = strdup(yytext); return AUXILIARY;
+"VTYSH_NODESWITCH"             value = strdup(yytext); return AUXILIARY;
+
+[ \t\n]+                       /* ignore */
+\\                             /* ignore */
+{ID}                           BEGIN(INITIAL); value = strdup(yytext); return ID;
+{OPERATOR}                     BEGIN(INITIAL); value = strdup(yytext); return OPERATOR;
+{SPECIAL}                      BEGIN(INITIAL); value = strdup(yytext); return SPECIAL;
+.                              /* printf("-- '%s' in init\n", yytext); */ BEGIN(INITIAL); return yytext[0];
+
+%%
+
+static int yylex_clr(char **retbuf)
+{
+       int rv = def_yylex();
+       *retbuf = value;
+       value = NULL;
+       return rv;
+}
+
+static PyObject *get_args(void)
+{
+       PyObject *pyObj = PyList_New(0);
+       PyObject *pyArg = NULL;
+
+       char *tval;
+       int depth = 1;
+       int token;
+
+       while ((token = yylex_clr(&tval)) != YY_NULL) {
+               if (token == SPECIAL && tval[0] == '(') {
+                       free(tval);
+                       break;
+               }
+               if (token == COMMENT) {
+                       free(tval);
+                       continue;
+               }
+               fprintf(stderr, "invalid input!\n");
+               exit(1);
+       }
+
+       while ((token = yylex_clr(&tval)) != YY_NULL) {
+               if (token == COMMENT) {
+                       free(tval);
+                       continue;
+               }
+               if (token == SPECIAL) {
+                       if (depth == 1 && (tval[0] == ',' || tval[0] == ')')) {
+                               if (pyArg)
+                                       PyList_Append(pyObj, pyArg);
+                               pyArg = NULL;
+                               if (tval[0] == ')') {
+                                       free(tval);
+                                       break;
+                               }
+                               free(tval);
+                               continue;
+                       }
+                       if (tval[0] == '(')
+                               depth++;
+                       if (tval[0] == ')')
+                               depth--;
+               }
+               if (!pyArg)
+                       pyArg = PyList_New(0);
+               PyList_Append(pyArg, PyUnicode_FromString(tval));
+               free(tval);
+       }
+       return pyObj;
+}
+
+/* _clippy.parse() -- read a C file, returning a list of interesting bits.
+ * note this ditches most of the actual C code. */
+PyObject *clippy_parse(PyObject *self, PyObject *args)
+{
+       const char *filename;
+       if (!PyArg_ParseTuple(args, "s", &filename))
+               return NULL;
+       
+       FILE *fd = fopen(filename, "r");
+       if (!fd)
+               return PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename);
+
+       char *tval;
+       int token;
+       yyin = fd;
+       value = NULL;
+
+       PyObject *pyCont = PyDict_New();
+       PyObject *pyObj = PyList_New(0);
+       PyDict_SetItemString(pyCont, "filename", PyUnicode_FromString(filename));
+       PyDict_SetItemString(pyCont, "data", pyObj);
+
+       while ((token = yylex_clr(&tval)) != YY_NULL) {
+                int lineno = yylineno;
+               PyObject *pyItem = NULL, *pyArgs;
+               switch (token) {
+               case DEFUNNY:
+               case INSTALL:
+               case AUXILIARY:
+                       pyArgs = get_args();
+                       pyItem = PyDict_New();
+                       PyDict_SetItemString(pyItem, "type", PyUnicode_FromString(tval));
+                       PyDict_SetItemString(pyItem, "args", pyArgs);
+                       break;
+               case COMMENT:
+                        if (strncmp(tval, "//~", 3) && strncmp(tval, "/*~", 3))
+                                break;
+                       pyItem = PyDict_New();
+                       PyDict_SetItemString(pyItem, "type", PyUnicode_FromString("COMMENT"));
+                       PyDict_SetItemString(pyItem, "line", PyUnicode_FromString(tval));
+                       break;
+               case PREPROC:
+                       pyItem = PyDict_New();
+                       PyDict_SetItemString(pyItem, "type", PyUnicode_FromString("PREPROC"));
+                       PyDict_SetItemString(pyItem, "line", PyUnicode_FromString(tval));
+                       break;
+               }
+               if (pyItem) {
+                       PyDict_SetItemString(pyItem, "lineno", PyLong_FromLong(lineno));
+                       PyList_Append(pyObj, pyItem);
+               }
+               free(tval);
+       }
+       def_yylex_destroy();
+       fclose(fd);
+       return pyCont;
+}