Merge pull request #714 from opensourcerouting/cli_magic_defpy

CLI magic: part 1 (DEFPY)
This commit is contained in:
Donald Sharp 2017-06-28 09:48:33 -04:00 committed by GitHub
commit 1e84e9a697
27 changed files with 1686 additions and 1383 deletions

1
.gitignore vendored
View File

@ -61,6 +61,7 @@ debian/quagga.prerm.debhelper
debian/quagga.substvars
debian/quagga/
debian/tmp/
*.pyc
*.swp
cscope.*
*.pb.h

View File

@ -9,7 +9,9 @@ SUBDIRS = lib qpb fpm @ZEBRA@ @LIBRFP@ @RFPTEST@ \
DIST_SUBDIRS = lib qpb fpm zebra bgpd ripd ripngd ospfd ospf6d ldpd \
isisd watchfrr vtysh ospfclient doc m4 pkgsrc redhat tests \
solaris pimd nhrpd eigrpd @LIBRFP@ @RFPTEST@ tools snapcraft \
babeld
babeld \
python \
# end
EXTRA_DIST = aclocal.m4 SERVICES REPORTING-BUGS \
update-autotools \

View File

@ -1,6 +1,8 @@
## Process this file with automake to produce Makefile.in.
AUTOMAKE_OPTIONS = subdir-objects
include ../common.am
if ENABLE_BGP_VNC
#o file to keep linker happy
BGP_VNC_RFP_LIB=rfapi/rfapi_descriptor_rfp_utils.o @top_builddir@/$(LIBRFP)/librfp.a
@ -116,5 +118,8 @@ examplesdir = $(exampledir)
dist_examples_DATA = bgpd.conf.sample bgpd.conf.sample2 \
bgpd.conf.vnc.sample
bgp_vty.o: bgp_vty_clippy.c
bgp_debug.o: bgp_debug_clippy.c
EXTRA_DIST = BGP4-MIB.txt

View File

@ -299,7 +299,7 @@ bgp_debug_list_conf_print (struct vty *vty, const char *desc, struct list *list)
}
static void
bgp_debug_list_add_entry(struct list *list, const char *host, struct prefix *p)
bgp_debug_list_add_entry(struct list *list, const char *host, const struct prefix *p)
{
struct bgp_debug_filter *filter;
@ -313,7 +313,8 @@ bgp_debug_list_add_entry(struct list *list, const char *host, struct prefix *p)
else if (p)
{
filter->host = NULL;
filter->p = p;
filter->p = prefix_new();
prefix_copy (filter->p, p);
}
listnode_add(list, filter);
@ -347,7 +348,7 @@ bgp_debug_list_remove_entry(struct list *list, const char *host, struct prefix *
}
static int
bgp_debug_list_has_entry(struct list *list, const char *host, struct prefix *p)
bgp_debug_list_has_entry(struct list *list, const char *host, const struct prefix *p)
{
struct bgp_debug_filter *filter;
struct listnode *node, *nnode;
@ -898,10 +899,14 @@ DEFUN (no_debug_bgp_keepalive_peer,
return CMD_SUCCESS;
}
#ifndef VTYSH_EXTRACT_PL
#include "bgp_debug_clippy.c"
#endif
/* debug bgp bestpath */
DEFUN (debug_bgp_bestpath_prefix,
DEFPY (debug_bgp_bestpath_prefix,
debug_bgp_bestpath_prefix_cmd,
"debug bgp bestpath <A.B.C.D/M|X:X::X:X/M>",
"debug bgp bestpath <A.B.C.D/M|X:X::X:X/M>$bestpath",
DEBUG_STR
BGP_STR
"BGP bestpath\n"
@ -909,30 +914,16 @@ DEFUN (debug_bgp_bestpath_prefix,
"IPv6 prefix\n")
{
int idx_ipv4_ipv6_prefixlen = 3;
struct prefix *argv_p;
int ret;
argv_p = prefix_new();
ret = str2prefix (argv[idx_ipv4_ipv6_prefixlen]->arg, argv_p);
if (!ret)
{
prefix_free(argv_p);
vty_out (vty, "%% Malformed Prefix%s", VTY_NEWLINE);
return CMD_WARNING;
}
if (!bgp_debug_bestpath_prefixes)
bgp_debug_bestpath_prefixes = list_new ();
if (bgp_debug_list_has_entry(bgp_debug_bestpath_prefixes, NULL, argv_p))
if (bgp_debug_list_has_entry(bgp_debug_bestpath_prefixes, NULL, bestpath))
{
vty_out (vty, "BGP bestptah debugging is already enabled for %s%s", argv[idx_ipv4_ipv6_prefixlen]->arg, VTY_NEWLINE);
vty_out (vty, "BGP bestpath debugging is already enabled for %s%s", bestpath_str, VTY_NEWLINE);
return CMD_SUCCESS;
}
bgp_debug_list_add_entry(bgp_debug_bestpath_prefixes, NULL, argv_p);
bgp_debug_list_add_entry(bgp_debug_bestpath_prefixes, NULL, bestpath);
if (vty->node == CONFIG_NODE)
{
@ -941,7 +932,7 @@ DEFUN (debug_bgp_bestpath_prefix,
else
{
TERM_DEBUG_ON (bestpath, BESTPATH);
vty_out (vty, "BGP bestpath debugging is on for %s%s", argv[idx_ipv4_ipv6_prefixlen]->arg, VTY_NEWLINE);
vty_out (vty, "BGP bestpath debugging is on for %s%s", bestpath_str, VTY_NEWLINE);
}
return CMD_SUCCESS;

View File

@ -777,6 +777,10 @@ bgp_clear_star_soft_out (struct vty *vty, const char *name)
}
#ifndef VTYSH_EXTRACT_PL
#include "bgp_vty_clippy.c"
#endif
/* BGP global configuration. */
DEFUN (bgp_multiple_instance_func,
@ -993,7 +997,7 @@ DEFUN (no_router_bgp,
/* BGP router-id. */
DEFUN (bgp_router_id,
DEFPY (bgp_router_id,
bgp_router_id_cmd,
"bgp router-id A.B.C.D",
BGP_STR
@ -1001,23 +1005,11 @@ DEFUN (bgp_router_id,
"Manually configured router identifier\n")
{
VTY_DECLVAR_CONTEXT(bgp, bgp);
int idx_ipv4 = 2;
int ret;
struct in_addr id;
ret = inet_aton (argv[idx_ipv4]->arg, &id);
if (! ret)
{
vty_out (vty, "%% Malformed bgp router identifier%s", VTY_NEWLINE);
return CMD_WARNING;
}
bgp_router_id_static_set (bgp, id);
bgp_router_id_static_set (bgp, router_id);
return CMD_SUCCESS;
}
DEFUN (no_bgp_router_id,
DEFPY (no_bgp_router_id,
no_bgp_router_id_cmd,
"no bgp router-id [A.B.C.D]",
NO_STR
@ -1026,28 +1018,18 @@ DEFUN (no_bgp_router_id,
"Manually configured router identifier\n")
{
VTY_DECLVAR_CONTEXT(bgp, bgp);
int idx_router_id = 3;
int ret;
struct in_addr id;
if (argc > idx_router_id)
if (router_id_str)
{
ret = inet_aton (argv[idx_router_id]->arg, &id);
if (! ret)
{
vty_out (vty, "%% Malformed BGP router identifier%s", VTY_NEWLINE);
return CMD_WARNING;
}
if (! IPV4_ADDR_SAME (&bgp->router_id_static, &id))
if (! IPV4_ADDR_SAME (&bgp->router_id_static, &router_id))
{
vty_out (vty, "%% BGP router-id doesn't match%s", VTY_NEWLINE);
return CMD_WARNING;
}
}
id.s_addr = 0;
bgp_router_id_static_set (bgp, id);
router_id.s_addr = 0;
bgp_router_id_static_set (bgp, router_id);
return CMD_SUCCESS;
}

View File

@ -3,6 +3,17 @@
# tree.
#
AM_V_CLIPPY = $(am__v_CLIPPY_$(V))
am__v_CLIPPY_ = $(am__v_CLIPPY_$(AM_DEFAULT_VERBOSITY))
am__v_CLIPPY_0 = @echo " CLIPPY " $@;
am__v_CLIPPY_1 =
SUFFIXES = _clippy.c
.c_clippy.c:
$(AM_V_at)$(MAKE) -C $(top_builddir)/$(CLIPPYDIR) clippy
$(AM_V_CLIPPY)$(top_builddir)/$(CLIPPYDIR)/clippy $(top_srcdir)/python/clidef.py $< > $@.tmp
@{ test -f $@ && diff $@.tmp $@ >/dev/null 2>/dev/null; } && rm $@.tmp || mv $@.tmp $@
if HAVE_PROTOBUF
# Uncomment to use an non-system version of libprotobuf-c.

View File

@ -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.
@ -123,6 +152,28 @@ AC_DEFUN([AC_C_FLAG], [{
AC_LANG_POP(C)
}])
AC_DEFUN([AC_LINK_IFELSE_FLAGS], [{
AC_LANG_PUSH(C)
ac_cflags_save="$CFLAGS"
ac_libs_save="$LIBS"
CFLAGS="$CFLAGS $1"
LIBS="$LIBS $2"
AC_LINK_IFELSE(
[$3],
[
AC_MSG_RESULT([yes])
CFLAGS="$ac_cflags_save"
LIBS="$ac_libs_save"
$5
], [
AC_MSG_RESULT([no])
CFLAGS="$ac_cflags_save"
LIBS="$ac_libs_save"
$4
])
AC_LANG_POP(C)
}])
dnl ICC won't bail on unknown options without -diag-error 10006
dnl need to do this first so we get useful results for the other options
AC_C_FLAG([-diag-error 10006])
@ -409,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.
#
@ -1422,7 +1544,9 @@ dnl ------------------
dnl check C-Ares library
dnl ------------------
if test "${NHRPD}" != ""; then
PKG_CHECK_MODULES([CARES], [libcares])
PKG_CHECK_MODULES([CARES], [libcares], , [
AC_MSG_ERROR([trying to build nhrpd, but libcares not found. install c-ares and its -dev headers.])
])
fi
@ -1437,15 +1561,14 @@ if test "${enable_snmp}" != ""; then
SNMP_LIBS="`${NETSNMP_CONFIG} --agent-libs`"
SNMP_CFLAGS="`${NETSNMP_CONFIG} --base-cflags`"
AC_MSG_CHECKING([whether we can link to Net-SNMP])
AC_LINK_IFELSE([AC_LANG_PROGRAM([
AC_LINK_IFELSE_FLAGS([$SNMP_CFLAGS], [$SNMP_LIBS], [AC_LANG_PROGRAM([
int main(void);
],
[
{
return 0;
}
])],[AC_MSG_RESULT(yes)],[
AC_MSG_RESULT(no)
])], [
AC_MSG_ERROR([--enable-snmp given but not usable])])
case "${enable_snmp}" in
yes)
@ -1852,6 +1975,7 @@ AC_CONFIG_FILES([Makefile lib/Makefile qpb/Makefile zebra/Makefile ripd/Makefile
redhat/Makefile
tools/Makefile
pkgsrc/Makefile
python/Makefile
fpm/Makefile
redhat/frr.spec
snapcraft/Makefile

2
debian/control vendored
View File

@ -3,7 +3,7 @@ Section: net
Priority: optional
Maintainer: Christian Hammers <ch@debian.org>
Uploaders: Florian Weimer <fw@debian.org>
Build-Depends: debhelper (>= 7.0.50~), libncurses5-dev, libreadline-dev, texlive-latex-base, texlive-generic-recommended, libpam0g-dev | libpam-dev, libcap-dev, texinfo (>= 4.7), imagemagick, ghostscript, groff, po-debconf, autotools-dev, hardening-wrapper, libpcre3-dev, gawk, chrpath, libsnmp-dev, git, dh-autoreconf, libjson0, libjson0-dev, dh-systemd, libsystemd-dev, python-ipaddr, bison, flex, libc-ares-dev
Build-Depends: debhelper (>= 7.0.50~), libncurses5-dev, libreadline-dev, texlive-latex-base, texlive-generic-recommended, libpam0g-dev | libpam-dev, libcap-dev, texinfo (>= 4.7), imagemagick, ghostscript, groff, po-debconf, autotools-dev, hardening-wrapper, libpcre3-dev, gawk, chrpath, libsnmp-dev, git, dh-autoreconf, libjson0, libjson0-dev, dh-systemd, libsystemd-dev, python-ipaddr, bison, flex, libc-ares-dev, python3-dev
Standards-Version: 3.9.6
Homepage: http://www.frr.net/
XS-Testsuite: autopkgtest

View File

@ -46,8 +46,7 @@ Install newer version of autoconf and automake (Package versions are too old)
sudo make install
cd ..
Install `Python 2.7` in parallel to default 2.6 (needed for `make check` to
run unittests).
Install `Python 2.7` in parallel to default 2.6
Make sure you've install EPEL (`epel-release` as above). Then install current
`python2.7` and `pytest`

View File

@ -107,6 +107,127 @@ Automatic assignment of variable names works by applying the following rules:
These rules should make it possible to avoid manual varname assignment in 90%
of the cases.
DEFPY
-----
`DEFPY(...)` is an enhanced version of `DEFUN()` which is preprocessed by
` python/clidef.py`. The python script parses the command definition string,
extracts variable names and types, and generates a C wrapper function that
parses the variables and passes them on. This means that in the CLI function
body, you will receive additional parameters with appropriate types.
This is best explained by an example:
```
DEFPY(func, func_cmd, "[no] foo bar A.B.C.D (0-99)$num", "...help...")
=>
func(self, vty, argc, argv, /* standard CLI arguments */
const char *no, /* unparsed "no" */
struct in_addr bar, /* parsed IP address */
const char *bar_str, /* unparsed IP address */
long num, /* parsed num */
const char *num_str) /* unparsed num */
```
Note that as documented in the previous section, "bar" is automatically
applied as variable name for "A.B.C.D". The python code then detects this
is an IP address argument and generates code to parse it into a
`struct in_addr`, passing it in `bar`. The raw value is passed in `bar_str`.
The range/number argument works in the same way with the explicitly given
variable name.
### Type rules
| Token(s) | Type | Value if omitted by user |
|--------------------------|-------------|--------------------------|
| `A.B.C.D` | `struct in_addr` | 0.0.0.0 |
| `X:X::X:X` | `struct in6_addr` | :: |
| `A.B.C.D + X:X::X:X` | `const union sockunion *` | NULL |
| `A.B.C.D/M` | `const struct prefix_ipv4 *` | NULL |
| `X:X::X:X/M` | `const struct prefix_ipv6 *` | NULL |
| `A.B.C.D/M + X:X::X:X/M` | `const struct prefix *` | NULL |
| `(0-9)` | `long` | 0 |
| `VARIABLE` | `const char *` | NULL |
| `word` | `const char *` | NULL |
| _all other_ | `const char *` | NULL |
Note the following details:
* not all parameters are pointers, some are passed as values.
* when the type is not `const char *`, there will be an extra `_str` argument
with type `const char *`.
* you can give a variable name not only to `VARIABLE` tokens but also to
`word` tokens (e.g. constant words). This is useful if some parts of a
command are optional. The type will be `const char *`.
* `[no]` will be passed as `const char *no`.
* pointers will be NULL when the argument is optional and the user did not
use it.
* if a parameter is not a pointer, but is optional and the user didn't use it,
the default value will be passed. Check the `_str` argument if you need to
determine whether the parameter was omitted.
* if the definition contains multiple parameters with the same variable name,
they will be collapsed into a single function parameter. The python code
will detect if the types are compatible (i.e. IPv4 + IPv6 variantes) and
choose a corresponding C type.
* the standard DEFUN parameters (self, vty, argc, argv) are still present and
can be used. A DEFUN can simply be **edited into a DEFPY without further
changes and it will still work**; this allows easy forward migration.
* a file may contain both DEFUN and DEFPY statements.
### Getting a parameter dump
The clidef.py script can be called to get a list of DEFUNs/DEFPYs with
the parameter name/type list:
```
lib/clippy python/clidef.py --all-defun --show lib/plist.c > /dev/null
```
The generated code is printed to stdout, the info dump to stderr. The
`--all-defun` argument will make it process DEFUN blocks as well as DEFPYs,
which is useful prior to converting some DEFUNs. **The dump does not list
the `_str` arguments** to keep the output shorter.
Note that the clidef.py script cannot be run with python directly, it needs
to be run with _clippy_ since the latter makes the CLI parser available.
### Include & Makefile requirements
A source file that uses DEFPY needs to include the `_clippy.c` file **before
all DEFPY statements**:
```
/* GPL header */
#include ...
...
#include "filename_clippy.c"
DEFPY(...)
DEFPY(...)
install_element(...)
```
This dependency needs to be marked in Makefile.am: (there is no ordering
requirement)
```
include ../common.am
# ...
# if linked into a LTLIBRARY (.la/.so):
filename.lo: filename_clippy.c
# if linked into an executable or static library (.a):
filename.o: filename_clippy.c
```
Doc Strings
-----------
Each token in a command definition should be documented with a brief doc

View File

@ -1,5 +1,7 @@
## Process this file with automake to produce Makefile.in.
include ../common.am
AM_CPPFLAGS = -I.. -I$(top_srcdir) -I$(top_srcdir)/lib -I$(top_builddir)/lib
AM_CFLAGS = $(WERROR)
DEFS = @DEFS@ -DSYSCONFDIR=\"$(sysconfdir)/\"
@ -9,6 +11,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
@ -87,13 +90,36 @@ 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
plist.lo: plist_clippy.c
EXTRA_DIST = \
queue.h \
command_lex.h \

137
lib/clippy.c Normal file
View File

@ -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();
}

28
lib/clippy.h Normal file
View File

@ -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 */

View File

@ -207,6 +207,10 @@ struct cmd_node
int argc __attribute__ ((unused)), \
struct cmd_token *argv[] __attribute__ ((unused)) )
#define DEFPY(funcname, cmdname, cmdstr, helpstr) \
DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, 0, 0) \
funcdecl_##funcname
#define DEFUN(funcname, cmdname, cmdstr, helpstr) \
DEFUN_CMD_FUNC_DECL(funcname) \
DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, 0, 0) \
@ -274,6 +278,9 @@ struct cmd_node
#define ALIAS_SH_DEPRECATED(daemon, funcname, cmdname, cmdstr, helpstr) \
DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, CMD_ATTR_DEPRECATED, daemon)
#else /* VTYSH_EXTRACT_PL */
#define DEFPY(funcname, cmdname, cmdstr, helpstr) \
DEFUN(funcname, cmdname, cmdstr, helpstr)
#endif /* VTYSH_EXTRACT_PL */
/* Some macroes */

View File

@ -44,6 +44,8 @@
* struct parser_ctx is needed for the bison forward decls.
*/
%code requires {
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
@ -189,7 +191,7 @@ start:
varname_token: '$' WORD
{
$$ = XSTRDUP (MTYPE_LEX, $2);
$$ = $2;
}
| /* empty */
{

336
lib/command_py.c Normal file
View File

@ -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);
}

265
lib/defun_lex.l Normal file
View File

@ -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;
}

File diff suppressed because it is too large Load Diff

3
python/Makefile.am Normal file
View File

@ -0,0 +1,3 @@
EXTRA_DIST = \
clidef.py \
clippy/__init__.py

254
python/clidef.py Normal file
View File

@ -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)

64
python/clippy/__init__.py Normal file
View File

@ -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)

View File

@ -143,6 +143,12 @@ Requires(post): /sbin/install-info
BuildRequires: gcc texi2html texinfo patch libcap-devel groff
BuildRequires: readline readline-devel ncurses ncurses-devel
BuildRequires: json-c-devel bison >= 2.7 flex make
%if 0%{?rhel} && 0%{?rhel} < 7
#python27-devel is available from ius community repo for RedHat/CentOS 6
BuildRequires: python27-devel
%else
BuildRequires: python-devel >= 2.7
%endif
Requires: ncurses json-c initscripts
%if %{with_pam}
BuildRequires: pam-devel

View File

@ -1,3 +1,5 @@
include ../common.am
PYTHON ?= python
AUTOMAKE_OPTIONS = subdir-objects
@ -28,6 +30,8 @@ else
BGP_VNC_RFP_LIB =
endif
lib/cli/test_cli.o: lib/cli/test_cli_clippy.c
check_PROGRAMS = \
lib/test_buffer \
lib/test_checksum \

View File

@ -39,7 +39,7 @@ int dump_args(struct vty *vty, const char *descr,
vty_out (vty, "%s with %d args.%s", descr, argc, VTY_NEWLINE);
for (i = 0; i < argc; i++)
{
vty_out (vty, "[%02d]: %s%s", i, argv[i]->arg, VTY_NEWLINE);
vty_out (vty, "[%02d] %s@%s: %s%s", i, argv[i]->text, argv[i]->varname, argv[i]->arg, VTY_NEWLINE);
}
return CMD_SUCCESS;

View File

@ -21,21 +21,38 @@
#include <zebra.h>
#include "prefix.h"
#include "common_cli.h"
DUMMY_DEFUN(cmd0, "arg ipv4 A.B.C.D");
DUMMY_DEFUN(cmd1, "arg ipv4m A.B.C.D/M");
DUMMY_DEFUN(cmd2, "arg ipv6 X:X::X:X");
DUMMY_DEFUN(cmd2, "arg ipv6 X:X::X:X$foo");
DUMMY_DEFUN(cmd3, "arg ipv6m X:X::X:X/M");
DUMMY_DEFUN(cmd4, "arg range (5-15)");
DUMMY_DEFUN(cmd5, "pat a < a|b>");
DUMMY_DEFUN(cmd6, "pat b <a|b A.B.C.D$bar>");
DUMMY_DEFUN(cmd7, "pat c <a | b|c> A.B.C.D");
DUMMY_DEFUN(cmd8, "pat d { foo A.B.C.D|bar X:X::X:X| baz }");
DUMMY_DEFUN(cmd8, "pat d { foo A.B.C.D$foo|bar X:X::X:X$bar| baz } [final]");
DUMMY_DEFUN(cmd9, "pat e [ WORD ]");
DUMMY_DEFUN(cmd10, "pat f [key]");
DUMMY_DEFUN(cmd11, "alt a WORD");
DUMMY_DEFUN(cmd12, "alt a A.B.C.D");
DUMMY_DEFUN(cmd13, "alt a X:X::X:X");
DUMMY_DEFUN(cmd14, "pat g { foo A.B.C.D$foo|foo|bar X:X::X:X$bar| baz } [final]");
#include "tests/lib/cli/test_cli_clippy.c"
DEFPY(magic_test, magic_test_cmd,
"magic (0-100) {ipv4net A.B.C.D/M|X:X::X:X$ipv6}",
"1\n2\n3\n4\n5\n")
{
char buf[256];
vty_out(vty, "def: %s%s", self->string, VTY_NEWLINE);
vty_out(vty, "num: %ld%s", magic, VTY_NEWLINE);
vty_out(vty, "ipv4: %s%s", prefix2str(ipv4net, buf, sizeof(buf)), VTY_NEWLINE);
vty_out(vty, "ipv6: %s%s", inet_ntop(AF_INET6, &ipv6, buf, sizeof(buf)), VTY_NEWLINE);
return CMD_SUCCESS;
}
void test_init(int argc, char **argv)
{
@ -47,6 +64,7 @@ void test_init(int argc, char **argv)
install_element (ENABLE_NODE, &cmd3_cmd);
install_element (ENABLE_NODE, &cmd4_cmd);
install_element (ENABLE_NODE, &cmd5_cmd);
install_element (ENABLE_NODE, &cmd6_cmd);
install_element (ENABLE_NODE, &cmd7_cmd);
install_element (ENABLE_NODE, &cmd8_cmd);
install_element (ENABLE_NODE, &cmd9_cmd);
@ -62,4 +80,6 @@ void test_init(int argc, char **argv)
uninstall_element (ENABLE_NODE, &cmd13_cmd);
install_element (ENABLE_NODE, &cmd13_cmd);
}
install_element (ENABLE_NODE, &cmd14_cmd);
install_element (ENABLE_NODE, &magic_test_cmd);
}

View File

@ -9,16 +9,16 @@ test# echo
test#
test# arg ipv4 1.2.3.4
cmd0 with 3 args.
[00]: arg
[01]: ipv4
[02]: 1.2.3.4
[00] arg@(null): arg
[01] ipv4@(null): ipv4
[02] A.B.C.D@ipv4: 1.2.3.4
test# arg ipv4 1.2.
A.B.C.D 02
test# arg ipv4 1.2.3.4
cmd0 with 3 args.
[00]: arg
[01]: ipv4
[02]: 1.2.3.4
[00] arg@(null): arg
[01] ipv4@(null): ipv4
[02] A.B.C.D@ipv4: 1.2.3.4
test# arg ipv4 1.2.3
% [NONE] Unknown command: arg ipv4 1.2.3
test# arg ipv4 1.2.3.4.5
@ -30,16 +30,16 @@ test# arg ipv4 blah
test#
test# arg ipv4m 1.2.3.0/24
cmd1 with 3 args.
[00]: arg
[01]: ipv4m
[02]: 1.2.3.0/24
[00] arg@(null): arg
[01] ipv4m@(null): ipv4m
[02] A.B.C.D/M@ipv4m: 1.2.3.0/24
test# arg ipv4m 1.2.
A.B.C.D/M 02
test# arg ipv4m 1.2.3.0/24
cmd1 with 3 args.
[00]: arg
[01]: ipv4m
[02]: 1.2.3.0/24
[00] arg@(null): arg
[01] ipv4m@(null): ipv4m
[02] A.B.C.D/M@ipv4m: 1.2.3.0/24
test# arg ipv4m 1.2.3/9
% [NONE] Unknown command: arg ipv4m 1.2.3/9
test# arg ipv4m 1.2.3.4.5/6
@ -57,35 +57,35 @@ test# arg ipv4m 1.2.3.0/9a
test#
test# arg ipv6 de4d:b33f::cafe
cmd2 with 3 args.
[00]: arg
[01]: ipv6
[02]: de4d:b33f::cafe
[00] arg@(null): arg
[01] ipv6@(null): ipv6
[02] X:X::X:X@foo: de4d:b33f::cafe
test# arg ipv6 de4d:b3
X:X::X:X 02
test# arg ipv6 de4d:b33f::caf
X:X::X:X 02
test# arg ipv6 de4d:b33f::cafe
cmd2 with 3 args.
[00]: arg
[01]: ipv6
[02]: de4d:b33f::cafe
[00] arg@(null): arg
[01] ipv6@(null): ipv6
[02] X:X::X:X@foo: de4d:b33f::cafe
test# arg ipv6 de4d:b3
test# arg ipv6 de4d:b33f::caf
X:X::X:X 02
test# arg ipv6 de4d:b33f::cafe
cmd2 with 3 args.
[00]: arg
[01]: ipv6
[02]: de4d:b33f::cafe
[00] arg@(null): arg
[01] ipv6@(null): ipv6
[02] X:X::X:X@foo: de4d:b33f::cafe
test# arg ipv6 de4d:b33f:z::cafe
% [NONE] Unknown command: arg ipv6 de4d:b33f:z::cafe
test# arg ipv6 de4d:b33f:cafe:
% [NONE] Unknown command: arg ipv6 de4d:b33f:cafe:
test# arg ipv6 ::
cmd2 with 3 args.
[00]: arg
[01]: ipv6
[02]: ::
[00] arg@(null): arg
[01] ipv6@(null): ipv6
[02] X:X::X:X@foo: ::
test# arg ipv6 ::/
% [NONE] Unknown command: arg ipv6 ::/
test# arg ipv6 1:2:3:4:5:6:7:8:9:0:1:2:3:4:5:6:7:8:9:0:1:2:3:4:5:6:7:8:9:0
@ -94,38 +94,38 @@ test# arg ipv6 12::34::56
% [NONE] Unknown command: arg ipv6 12::34::56
test# arg ipv6m dead:beef:cafe::/64
cmd3 with 3 args.
[00]: arg
[01]: ipv6m
[02]: dead:beef:cafe::/64
[00] arg@(null): arg
[01] ipv6m@(null): ipv6m
[02] X:X::X:X/M@ipv6m: dead:beef:cafe::/64
test# arg ipv6m dead:be
X:X::X:X/M 02
test# arg ipv6m dead:beef:cafe:
X:X::X:X/M 02
test# arg ipv6m dead:beef:cafe::/64
cmd3 with 3 args.
[00]: arg
[01]: ipv6m
[02]: dead:beef:cafe::/64
[00] arg@(null): arg
[01] ipv6m@(null): ipv6m
[02] X:X::X:X/M@ipv6m: dead:beef:cafe::/64
test#
test# arg range 4
% [NONE] Unknown command: arg range 4
test# arg range 5
cmd4 with 3 args.
[00]: arg
[01]: range
[02]: 5
[00] arg@(null): arg
[01] range@(null): range
[02] (5-15)@range: 5
test# arg range 9
(5-15) 02
test# arg range 9
cmd4 with 3 args.
[00]: arg
[01]: range
[02]: 9
[00] arg@(null): arg
[01] range@(null): range
[02] (5-15)@range: 9
test# arg range 15
cmd4 with 3 args.
[00]: arg
[01]: range
[02]: 15
[00] arg@(null): arg
[01] range@(null): range
[02] (5-15)@range: 15
test# arg range 16
% [NONE] Unknown command: arg range 16
test# arg range -1
@ -146,7 +146,8 @@ test# pa
test# papat
% Command incomplete.
test# pat
a c d e f
a b c d e f
g
test# pat
% Command incomplete.
test#
@ -154,17 +155,17 @@ test# pat a
% Command incomplete.
test# pat a a
cmd5 with 3 args.
[00]: pat
[01]: a
[02]: a
[00] pat@(null): pat
[01] a@(null): a
[02] a@(null): a
test# pat a
a 02
b 03
test# pat a b
cmd5 with 3 args.
[00]: pat
[01]: a
[02]: b
[00] pat@(null): pat
[01] a@(null): a
[02] b@(null): b
test# pat a c
% There is no matched command.
test# pat a c
@ -176,10 +177,10 @@ test# pat c a
% Command incomplete.
test# pat c a 1.2.3.4
cmd7 with 4 args.
[00]: pat
[01]: c
[02]: a
[03]: 1.2.3.4
[00] pat@(null): pat
[01] c@(null): c
[02] a@(null): a
[03] A.B.C.D@(null): 1.2.3.4
test# pat c b 2.3.4
% [NONE] Unknown command: pat c b 2.3.4
test# pat c c
@ -195,72 +196,72 @@ test# pat d
% Command incomplete.
test# pat d foo 1.2.3.4
cmd8 with 4 args.
[00]: pat
[01]: d
[02]: foo
[03]: 1.2.3.4
[00] pat@(null): pat
[01] d@(null): d
[02] foo@(null): foo
[03] A.B.C.D@foo: 1.2.3.4
test# pat d foo
% Command incomplete.
test# pat d noooo
% [NONE] Unknown command: pat d noooo
test# pat d bar 1::2
cmd8 with 4 args.
[00]: pat
[01]: d
[02]: bar
[03]: 1::2
[00] pat@(null): pat
[01] d@(null): d
[02] bar@(null): bar
[03] X:X::X:X@bar: 1::2
test# pat d bar 1::2 foo 3.4.5.6
cmd8 with 6 args.
[00]: pat
[01]: d
[02]: bar
[03]: 1::2
[04]: foo
[05]: 3.4.5.6
[00] pat@(null): pat
[01] d@(null): d
[02] bar@(null): bar
[03] X:X::X:X@bar: 1::2
[04] foo@(null): foo
[05] A.B.C.D@foo: 3.4.5.6
test# pat d ba
bar 04
baz 06
test# pat d baz
cmd8 with 3 args.
[00]: pat
[01]: d
[02]: baz
[00] pat@(null): pat
[01] d@(null): d
[02] baz@(null): baz
test# pat d foo 3.4.5.6 baz
cmd8 with 5 args.
[00]: pat
[01]: d
[02]: foo
[03]: 3.4.5.6
[04]: baz
[00] pat@(null): pat
[01] d@(null): d
[02] foo@(null): foo
[03] A.B.C.D@foo: 3.4.5.6
[04] baz@(null): baz
test#
test# pat e
cmd9 with 2 args.
[00]: pat
[01]: e
[00] pat@(null): pat
[01] e@(null): e
test# pat e f
cmd9 with 3 args.
[00]: pat
[01]: e
[02]: f
[00] pat@(null): pat
[01] e@(null): e
[02] WORD@e: f
test# pat e f g
% [NONE] Unknown command: pat e f g
test# pat e 1.2.3.4
cmd9 with 3 args.
[00]: pat
[01]: e
[02]: 1.2.3.4
[00] pat@(null): pat
[01] e@(null): e
[02] WORD@e: 1.2.3.4
test#
test# pat f
cmd10 with 2 args.
[00]: pat
[01]: f
[00] pat@(null): pat
[01] f@(null): f
test# pat f foo
% [NONE] Unknown command: pat f foo
test# pat f key
cmd10 with 3 args.
[00]: pat
[01]: f
[02]: key
[00] pat@(null): pat
[01] f@(null): f
[02] key@(null): key
test#
test# alt a
test# alt a a
@ -268,18 +269,18 @@ test# alt a a
X:X::X:X 02
test# alt a ab
cmd11 with 3 args.
[00]: alt
[01]: a
[02]: ab
[00] alt@(null): alt
[01] a@(null): a
[02] WORD@a: ab
test# alt a 1
test# alt a 1.2
A.B.C.D 02
WORD 02
test# alt a 1.2.3.4
cmd12 with 3 args.
[00]: alt
[01]: a
[02]: 1.2.3.4
[00] alt@(null): alt
[01] a@(null): a
[02] A.B.C.D@a: 1.2.3.4
test# alt a 1
test# alt a 1:2
WORD 02
@ -290,16 +291,16 @@ test# alt a 1:2::
X:X::X:X 02
test# alt a 1:2::3
cmd13 with 3 args.
[00]: alt
[01]: a
[02]: 1:2::3
[00] alt@(null): alt
[01] a@(null): a
[02] X:X::X:X@a: 1:2::3
test#
test# conf t
test(config)# do pat d baz
cmd8 with 3 args.
[00]: pat
[01]: d
[02]: baz
[00] pat@(null): pat
[01] d@(null): d
[02] baz@(null): baz
test(config)# exit
test#
test# show run

View File

@ -67,10 +67,10 @@ zebra_fpm_la_SOURCES += zebra_fpm_netlink.c
endif
if HAVE_PROTOBUF
zebra_fpm_la_SOURCES += zebra_fpm_protobuf.c
endif
if DEV_BUILD
zebra_fpm_la_SOURCES += zebra_fpm_dt.c
endif
endif
EXTRA_DIST = if_ioctl.c if_ioctl_solaris.c if_netlink.c \