Bruce Richardson, Oct 11, 2023 at 15:33:
Provide a "dpdk-cmdline-gen.py" script for application developers to
quickly generate the boilerplate code necessary for using the cmdline
library.

Example of use:
The script takes an input file with a list of commands the user wants in
the app, where the parameter variables are tagged with the type.
For example:

        $ cat commands.list
        list
        add <UINT16>x <UINT16>y
        echo <STRING>message
        add socket <STRING>path
        quit

When run through the script as "./dpdk-cmdline-gen.py commands.list",
the output will be the contents of a header file with all the
boilerplate necessary for a commandline instance with those commands.

If the flag --stubs is passed, an output header filename must also be
passed, in which case both a header file with the definitions and a C
file with function stubs in it is written to disk. The separation is so
that the header file can be rewritten at any future point to add more
commands, while the C file can be kept as-is and extended by the user
with any additional functions needed.

Signed-off-by: Bruce Richardson <bruce.richard...@intel.com>
---

Hi Bruce,

this is a nice addition, I have a few python style remarks below.

In general, I would advise formatting your code with black[1] to avoid debates over coding style. It makes all code homogeneous and lets you focus on more important things :)

 buildtools/dpdk-cmdline-gen.py    | 167 ++++++++++++++++++++++++++++++
 buildtools/meson.build            |   7 ++
 doc/guides/prog_guide/cmdline.rst | 131 ++++++++++++++++++++++-
 3 files changed, 304 insertions(+), 1 deletion(-)
 create mode 100755 buildtools/dpdk-cmdline-gen.py

diff --git a/buildtools/dpdk-cmdline-gen.py b/buildtools/dpdk-cmdline-gen.py
new file mode 100755
index 0000000000..3b41fb0493
--- /dev/null
+++ b/buildtools/dpdk-cmdline-gen.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 Intel Corporation
+#
+"""Script to automatically generate boilerplate for using DPDK cmdline 
library."""

Multi line (or single line) doc strings are usually formatted as follows:

"""
Script to automatically generate boilerplate for using DPDK cmdline library.
"""

It makes adding new lines more readable and saves a bit of characters per line.

+
+import argparse
+import sys
+
+PARSE_FN_PARAMS = 'void *parsed_result, struct cmdline *cl, void *data'
+PARSE_FN_BODY = """
+    /* TODO: command action */
+    RTE_SET_USED(parsed_result);
+    RTE_SET_USED(cl);
+    RTE_SET_USED(data);
+"""
+
+
+def process_command(tokens, cfile, comment):
+    """Generate the structures and definitions for a single command."""
+    name = []
+
+    if tokens[0].startswith('<'):
+        print('Error: each command must start with at least one literal 
string', file=sys.stderr)
+        sys.exit(1)

It would be better to raise an exception here and handle it in main() for error reporting.

+    for t in tokens:
+        if t.startswith('<'):
+            break
+        name.append(t)
+    name = '_'.join(name)
+
+    result_struct = []
+    initializers = []
+    token_list = []
+    for t in tokens:
+        if t.startswith('<'):
+            t_type, t_name = t[1:].split('>')
+            t_val = 'NULL'
+        else:
+            t_type = 'STRING'
+            t_name = t
+            t_val = f'"{t}"'
+
+        if t_type == 'STRING':
+            result_struct.append(f'\tcmdline_fixed_string_t {t_name};')
+            initializers.append(
+                    f'static cmdline_parse_token_string_t 
cmd_{name}_{t_name}_tok =\n' +
+                    f'\tTOKEN_STRING_INITIALIZER(struct cmd_{name}_result, 
{t_name}, {t_val});')
+        elif t_type in ['UINT8', 'UINT16', 'UINT32', 'UINT64', 'INT8', 
'INT16', 'INT32', 'INT64']:
+            result_struct.append(f'\t{t_type.lower()}_t {t_name};')
+            initializers.append(
+                    f'static cmdline_parse_token_num_t cmd_{name}_{t_name}_tok 
=\n' +
+                    f'\tTOKEN_NUM_INITIALIZER(struct cmd_{name}_result, 
{t_name}, RTE_{t_type});')
+        elif t_type in ['IP', 'IP_ADDR', 'IPADDR']:
+            result_struct.append(f'\tcmdline_ipaddr_t {t_name};')
+            initializers.append(
+                    f'cmdline_parse_token_ipaddr_t cmd_{name}_{t_name}_tok 
=\n' +
+                    f'\tTOKEN_IPV4_INITIALIZER(struct cmd_{name}_result, 
{t_name});')
+        else:
+            print(f'Error: unknown token-type {t}', file=sys.stderr)
+            sys.exit(1)
+        token_list.append(f'cmd_{name}_{t_name}_tok')
+
+    print(f'/* Auto-generated handling for command "{" ".join(tokens)}" */')
+    # output function prototype
+    func_sig = f'void\ncmd_{name}_parsed({PARSE_FN_PARAMS})'
+    print(f'extern {func_sig};\n')
+    # output function template if C file being written
+    if (cfile):
+        print(f'{func_sig}\n{{{PARSE_FN_BODY}}}\n', file=cfile)
+    # output result data structure
+    print(
+            f'struct cmd_{name}_result {{\n' +
+            '\n'.join(result_struct) +
+            '\n};\n')
+    # output the initializer tokens
+    print('\n'.join(initializers) + '\n')
+    # output the instance structure
+    print(
+            f'static cmdline_parse_inst_t cmd_{name} = {{\n' +
+            f'\t.f = cmd_{name}_parsed,\n' +
+            '\t.data = NULL,\n' +
+            f'\t.help_str = "{comment}",\n' +
+            '\t.tokens = {')
+    for t in token_list:
+        print(f'\t\t(void *)&{t},')
+    print('\t\tNULL\n' + '\t}\n' + '};\n')
+
+    # return the instance structure name
+    return f'cmd_{name}'
+
+
+def process_commands(infile, hfile, cfile, ctxname):
+    """Generate boilerplate output for a list of commands from infile."""
+    instances = []
+
+    # redirect stdout to output the header, to save passing file= each print
+    old_sys_stdout = sys.stdout
+    sys.stdout = hfile

Why not use hfile.write()?

I think the main issue here is to use print() in process_commands(). It would probably be cleaner to have process_command() return a list of lines and print them in this function.

+
+    print(f'/* File autogenerated by {sys.argv[0]} */')
+    print('#ifndef GENERATED_COMMANDS_H')
+    print('#define GENERATED_COMMANDS_H')
+    print('#include <rte_common.h>')
+    print('#include <cmdline.h>')
+    print('#include <cmdline_parse_string.h>')
+    print('#include <cmdline_parse_num.h>')
+    print('#include <cmdline_parse_ipaddr.h>')
+    print('')

You can use a multi-line f-string here with a single print/write.

   hfile.write(f"""/* File autogenerated by {sys.argv[0]} */
#ifndef GENERATED_COMMANDS_H
#define GENERATED_COMMANDS_H

#include <rte_common.h>
#include <cmdline.h>
#include <cmdline_parse_string.h>
#include <cmdline_parse_num.h>
#include <cmdline_parse_ipaddr.h>

""")

+
+    for line in infile.readlines():
+        if line.lstrip().startswith('#'):
+            continue
+        if '#' not in line:
+            line = line + '#'  # ensure split always works, even if no help 
text
+        tokens, comment = line.split('#', 1)
+        instances.append(process_command(tokens.strip().split(), cfile, 
comment.strip()))

If process_command returns a name and a list of lines, that could be transformed as:

          name, lines = process_command(tokens.strip().split(), cfile, 
comment.strip())
          instances.append(name)
          hfile.write("\n".join(lines) + "\n")

+
+    print(f'static __rte_used cmdline_parse_ctx_t {ctxname}[] = {{')
+    for inst in instances:
+        print(f'\t&{inst},')
+    print('\tNULL')
+    print('};\n')
+    print('#endif /* GENERATED_COMMANDS_H */')

also multi line print here:

hfile.write("""\tNULL
};
#endif /* GENERATED_COMMANDS_H */
""")

+
+    sys.stdout = old_sys_stdout
+
+
+def main():
+    """Application main entry point."""
+    ap = argparse.ArgumentParser()

Nit to have a nice description of the command with --help:

      ap = argparse.ArgumentParser(description=__doc__)

+    ap.add_argument(
+            '--stubs', action='store_true',
+            help='Produce C file with empty function stubs for each command')
+    ap.add_argument(
+            '--output-file', '-o', default='-',
+            help='Output header filename [default to stdout]')
+    ap.add_argument(
+            '--context-name', default='ctx',
+            help='Name given to the cmdline context variable in the output 
header [default=ctx]')
+    ap.add_argument(
+            'infile', type=argparse.FileType('r'),
+            help='File with list of commands')
+    args = ap.parse_args()
+
+    if not args.stubs:
+        if args.output_file == '-':
+            process_commands(args.infile, sys.stdout, None, args.context_name)
+        else:
+            with open(args.output_file, 'w') as hfile:
+                process_commands(args.infile, hfile, None, args.context_name)
+    else:
+        if not args.output_file.endswith('.h'):
+            print(
+                    'Error: output filename must end with ".h" extension when 
creating stubs',
+                    file=sys.stderr)
+            sys.exit(1)

You can replace print to stderr + exit with:

ap.error("-o/--output-file: must end with .h extension when creating stubs")

+
+        cfilename = args.output_file[:-2] + '.c'
+        with open(args.output_file, 'w') as hfile:
+            with open(cfilename, 'w') as cfile:
+                print(f'#include "{args.output_file}"\n', file=cfile)
+                process_commands(args.infile, hfile, cfile, args.context_name)
+
+
+if __name__ == '__main__':
+    main()

I'll stop here ;) Thanks!

Reply via email to