summaryrefslogtreecommitdiffstats
path: root/cligen
diff options
context:
space:
mode:
Diffstat (limited to 'cligen')
-rwxr-xr-xcligen/cli-codegen.py40
-rwxr-xr-xcligen/cli-docgen.py57
-rw-r--r--cligen/cligen.mk9
-rw-r--r--cligen/cligen/__init__.py0
-rw-r--r--cligen/cligen/code.py749
-rw-r--r--cligen/cligen/doc/__init__.py0
-rw-r--r--cligen/cligen/doc/man.py273
-rw-r--r--cligen/cligen/doc/texi.py218
-rw-r--r--cligen/cligen/package.py104
-rw-r--r--cligen/cligen/types.py183
10 files changed, 1633 insertions, 0 deletions
diff --git a/cligen/cli-codegen.py b/cligen/cli-codegen.py
new file mode 100755
index 0000000..afac576
--- /dev/null
+++ b/cligen/cli-codegen.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import argparse
+import cligen.package
+import cligen.types
+import cligen.code
+import json
+
+
+parser = argparse.ArgumentParser(description='generate option parsing code')
+parser.add_argument('json', type=argparse.FileType('r'))
+parser.add_argument('c', type=argparse.FileType('w'))
+parser.add_argument('h', type=argparse.FileType('w'))
+parser.add_argument('--package', help='package', required=True)
+parser.add_argument('--version', help='version', required=True)
+parser.add_argument('--license', help='license')
+parser.add_argument('--authors', help='authors')
+parser.add_argument('--copyright-year', help='copyright year')
+parser.add_argument('--copyright-holder', help='copyright holder')
+parser.add_argument('--bug-email', help='bug report email address')
+
+args = parser.parse_args()
+kwargs = {
+ 'name': args.package,
+ 'version': args.version
+}
+if args.license:
+ kwargs['license'] = args.license
+if args.copyright_year:
+ kwargs['copyright_year'] = args.copyright_year
+if args.copyright_holder:
+ kwargs['copyright_holder'] = args.copyright_holder
+if args.bug_email:
+ kwargs['bug_email'] = args.bug_email
+info = cligen.package.Info(**kwargs)
+desc = cligen.types.Desc.from_json(json.load(args.json))
+cligen.code.generate_source(desc, info, args.c)
+cligen.code.generate_header(desc, info, args.h)
diff --git a/cligen/cli-docgen.py b/cligen/cli-docgen.py
new file mode 100755
index 0000000..c3453ad
--- /dev/null
+++ b/cligen/cli-docgen.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import argparse
+import cligen.package
+import cligen.types
+import json
+import sys
+
+parser = argparse.ArgumentParser(description='generate documentation')
+parser.add_argument('json', type=argparse.FileType('r'))
+parser.add_argument('outfile', type=argparse.FileType('w'))
+parser.add_argument('--format', choices=['man', 'texi'])
+parser.add_argument('--level', type=int, default=0)
+parser.add_argument('--section-node', action='store_true')
+parser.add_argument('--include', action='append')
+parser.add_argument('--package', help='package', required=True)
+parser.add_argument('--version', help='version')
+parser.add_argument('--license', help='license')
+parser.add_argument('--authors', help='authors')
+parser.add_argument('--copyright-year', help='copyright year')
+parser.add_argument('--copyright-holder', help='copyright holder')
+parser.add_argument('--bug-email', help='bug report email address')
+
+args = parser.parse_args()
+kwargs = {
+ 'name': args.package,
+ 'version': args.version
+}
+if args.license:
+ kwargs['license'] = args.license
+if args.copyright_year:
+ kwargs['copyright_year'] = args.copyright_year
+if args.copyright_holder:
+ kwargs['copyright_holder'] = args.copyright_holder
+if args.bug_email:
+ kwargs['bug_email'] = args.bug_email
+info = cligen.package.Info(**kwargs)
+desc = cligen.types.Desc.from_json(json.load(args.json))
+
+includes = dict()
+if args.include:
+ for i in args.include:
+ (section, infile) = i.split('=')
+ includes[section] = open(infile, 'r')
+
+if args.format == 'man':
+ import cligen.doc.man
+ cligen.doc.man.generate(desc, info, includes, args.outfile)
+elif args.format == 'texi':
+ import cligen.doc.texi
+ cligen.doc.texi.generate(desc, info, includes, args.outfile,
+ level=args.level, section_node=args.section_node)
+else:
+ sys.stderr.write(f'Unknown format {args.format}\n')
+ sys.exit(1)
diff --git a/cligen/cligen.mk b/cligen/cligen.mk
new file mode 100644
index 0000000..87a79a6
--- /dev/null
+++ b/cligen/cligen.mk
@@ -0,0 +1,9 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+cligen_sources = \
+ cligen/cli-codegen.py cligen/cli-docgen.py \
+ cligen/cligen/__init__.py cligen/cligen/code.py \
+ cligen/cligen/package.py cligen/cligen/types.py \
+ cligen/cligen/doc/__init__.py cligen/cligen/doc/man.py \
+ cligen/cligen/doc/texi.py
diff --git a/cligen/cligen/__init__.py b/cligen/cligen/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cligen/cligen/__init__.py
diff --git a/cligen/cligen/code.py b/cligen/cligen/code.py
new file mode 100644
index 0000000..000c375
--- /dev/null
+++ b/cligen/cligen/code.py
@@ -0,0 +1,749 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from typing import Mapping, MutableMapping, MutableSequence, Sequence
+from typing import TextIO, Union
+import io
+from cligen.types import ArgumentType, Desc, OptionDesc
+from cligen.package import Info, version
+import sys
+import textwrap
+
+INDENT = ' '
+
+
+def get_aliases(options: Sequence[OptionDesc]) -> Mapping[str, Sequence[str]]:
+ aliases: MutableMapping[str, MutableSequence[str]] = dict()
+ for option in options:
+ if option.aliases:
+ val = aliases.get(option.aliases, list())
+ val.append(option.long_option)
+ aliases[option.aliases] = val
+ return aliases
+
+
+def get_chars(options: Sequence[OptionDesc]) -> Mapping[str, Union[str, int]]:
+ chars: MutableMapping[str, Union[str, int]] = dict()
+ chars_counter = 1
+ short_opts: MutableMapping[str, str] = dict()
+ for option in options:
+ # If the short option is already taken, do not register twice
+ if option.short_option and (option.short_option not in short_opts):
+ chars[option.long_option] = option.short_option
+ short_opts[option.short_option] = option.long_option
+ else:
+ if option.short_option:
+ print((f'short option {option.short_option} for '
+ f'{option.long_option} is already '
+ f'taken by {short_opts[option.short_option]}'),
+ file=sys.stderr)
+ chars[option.long_option] = chars_counter
+ chars_counter += 1
+ if option.disable_prefix:
+ chars[
+ f'{option.disable_prefix}{option.long_option}'
+ ] = chars_counter
+ chars_counter += 1
+ return chars
+
+
+# Reserved keywords in C, from 6.4.1 of N1570
+KEYWORDS = {
+ 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
+ 'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'inline',
+ 'int', 'long', 'register', 'restrict', 'return', 'short', 'signed',
+ 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned',
+ 'void', 'volatile', 'while', '_Alignas', '_Alignof', '_Atomic', '_Bool',
+ '_Complex', '_Generic', '_Imaginary', '_Noreturn', '_Static_assert',
+ '_Thread_local',
+}
+
+
+def escape_c_keyword(name: str) -> str:
+ while name in KEYWORDS:
+ name += '_'
+ return name
+
+
+def mangle(name: str) -> str:
+ return ''.join([c if c in 'abcdefghijklmnopqrstuvwxyz0123456789_' else '_'
+ for c in name.lower()])
+
+
+def format_long_opt(c: Union[str, int], long_opt: str, has_arg: str) -> str:
+ if isinstance(c, str):
+ return f"{INDENT}{{ \"{long_opt}\", {has_arg}, 0, '{c}' }},\n"
+ else:
+ return f'{INDENT}{{ "{long_opt}", {has_arg}, 0, CHAR_MAX + {c} }},\n'
+
+
+def format_switch_case(c: Union[str, int], long_opt: str) -> str:
+ if isinstance(c, str):
+ return f"{INDENT*3}case '{c}':\n"
+ else:
+ return f'{INDENT*3}case CHAR_MAX + {c}: /* --{long_opt} */\n'
+
+
+def usage(desc: Desc, info: Info) -> str:
+ out = io.StringIO()
+ out.write(f'{desc.tool.name} - {desc.tool.title}\n')
+ out.write(
+ f'Usage: {desc.tool.name} '
+ f'[ -<flag> [<val>] | --<name>[{{=| }}<val>] ]... '
+ f'{desc.tool.argument if desc.tool.argument else ""}\n'
+ )
+ for section in desc.sections:
+ out.write('\n')
+ if section.description != '':
+ out.write(f'{section.description}:\n\n')
+ for option in section.options:
+ if option.deprecated:
+ continue
+ if option.short_option:
+ header = f' -{option.short_option}, --{option.long_option}'
+ else:
+ header = f' --{option.long_option}'
+ if option.argument_type:
+ if option.argument_type:
+ arg = option.argument_type.get_name()
+ else:
+ arg = 'arg'
+ if option.argument_optional:
+ header += f'[={arg}]'
+ else:
+ header += f'={arg}'
+ if len(header) < 30:
+ header = header.ljust(30)
+ elif option.argument_type:
+ header += ' '
+ else:
+ header += ' '
+ if option.aliases:
+ out.write(f'{header}an alias for the '
+ f"'{option.aliases}' option\n")
+ else:
+ out.write(f'{header}{option.description}\n')
+ if len(option.conflicts) == 1:
+ out.write(
+ f"\t\t\t\t- prohibits the option '{option.conflicts[0]}'\n"
+ )
+ elif len(option.conflicts) > 1:
+ conflict_opts_concatenated = '\n'.join([
+ f'\t\t\t\t{o}' for o in option.conflicts
+ ])
+ out.write(
+ '\t\t\t\t- prohibits these options:\n' +
+ conflict_opts_concatenated + '\n'
+ )
+ if len(option.requires) == 1:
+ out.write(
+ f"\t\t\t\t- requires the option '{option.requires[0]}'\n"
+ )
+ elif len(option.requires) > 1:
+ require_opts_concatenated = '\n'.join([
+ f'\t\t\t\t{o}' for o in option.requires
+ ])
+ out.write(
+ '\t\t\t\t- requires these options:\n' +
+ require_opts_concatenated + '\n'
+ )
+ if option.file_exists:
+ out.write('\t\t\t\t- file must pre-exist\n')
+ if option.enabled:
+ out.write('\t\t\t\t- enabled by default\n')
+ if option.disable_prefix:
+ out.write(
+ '\t\t\t\t- disabled as '
+ f"'--{option.disable_prefix}{option.long_option}'\n"
+ )
+ if option.argument_range:
+ out.write(
+ '\t\t\t\t- it must be in the range:\n'
+ f'\t\t\t\t {option.argument_range.minimum} to '
+ f'{option.argument_range.maximum}\n'
+ )
+ out.write(textwrap.dedent('''
+ Options are specified by doubled hyphens and their name or by a single
+ hyphen and the flag character.
+ '''))
+ if desc.tool.argument:
+ out.write(('Operands and options may be intermixed. '
+ 'They will be reordered.\n'))
+ if desc.tool.detail:
+ out.write(f'\n{desc.tool.detail}\n')
+ if info.bug_email:
+ out.write(f'\nPlease send bug reports to: <{info.bug_email}>\n')
+ return out.getvalue()
+
+
+def generate_source(desc: Desc, info: Info, outfile: TextIO):
+ long_opts = io.StringIO()
+ short_opts = list()
+ default_definitions = io.StringIO()
+ default_statements = io.StringIO()
+ switch_cases = io.StringIO()
+ enable_statements = io.StringIO()
+ constraint_statements = io.StringIO()
+ has_list_arg = False
+ has_number_arg = False
+ has_default_arg = False
+
+ options = [option for section in desc.sections
+ for option in section.options]
+ chars = get_chars(options)
+ aliases = get_aliases(options)
+
+ struct_name = f'{mangle(desc.tool.name)}_options'
+ global_name = f'{mangle(desc.tool.name)}_options'
+
+ switch_cases.write(f"{INDENT*3}case '\\0': /* Long option. */\n")
+ switch_cases.write(f'{INDENT*4}break;\n')
+
+ for option in options:
+ long_opt = option.long_option
+ arg_type = option.argument_type
+ lower_opt = mangle(long_opt)
+ upper_opt = lower_opt.upper()
+ lower_opt = escape_c_keyword(lower_opt)
+
+ # aliases are handled differently
+ if option.aliases:
+ continue
+
+ if arg_type:
+ if option.argument_optional:
+ has_arg = 'optional_argument'
+ else:
+ has_arg = 'required_argument'
+ else:
+ has_arg = 'no_argument'
+
+ c = chars[long_opt]
+
+ if isinstance(c, str):
+ if arg_type:
+ short_opts.append(c + ':')
+ else:
+ short_opts.append(c)
+
+ long_opts.write(format_long_opt(c, long_opt, has_arg))
+ switch_cases.write(format_switch_case(c, long_opt))
+
+ for alias in aliases.get(long_opt, list()):
+ c = chars[alias]
+ long_opts.write(format_long_opt(c, alias, has_arg))
+ switch_cases.write(format_switch_case(c, alias))
+
+ switch_cases.write(f'{INDENT*4}opts->present.{lower_opt} = true;\n')
+
+ if arg_type:
+ if option.multiple:
+ has_list_arg = True
+ switch_cases.write((
+ f'{INDENT*4}append_to_list (&opts->list.{lower_opt}, '
+ f'"{long_opt}", optarg);\n'
+ ))
+ else:
+ switch_cases.write(
+ f'{INDENT*4}opts->arg.{lower_opt} = optarg;\n'
+ )
+ if arg_type == ArgumentType.NUMBER:
+ has_number_arg = True
+ switch_cases.write((
+ f'{INDENT*4}opts->value.{lower_opt} = '
+ 'parse_number(optarg);\n'
+ ))
+ if option.argument_default:
+ has_default_arg = True
+ default_definitions.write(
+ f'static const char *{lower_opt}_default = '
+ f'"{option.argument_default}";\n'
+ )
+ default_statements.write(
+ f'{INDENT}opts->arg.{lower_opt} = '
+ f'{lower_opt}_default;\n'
+ )
+ if arg_type == ArgumentType.NUMBER:
+ assert isinstance(option.argument_default, int)
+ default_statements.write((
+ f'{INDENT}opts->value.{lower_opt} = '
+ f'{option.argument_default};\n'
+ ))
+
+ switch_cases.write(
+ f'{INDENT*4}opts->enabled.{lower_opt} = true;\n'
+ )
+
+ switch_cases.write(f'{INDENT*4}break;\n')
+
+ if option.enabled:
+ enable_statements.write(
+ f'{INDENT}opts->enabled.{lower_opt} = true;\n'
+ )
+ if option.disable_prefix:
+ disable_opt = f'{option.disable_prefix}{long_opt}'
+ c = chars[disable_opt]
+ long_opts.write(format_long_opt(c, disable_opt, has_arg))
+ switch_cases.write(format_switch_case(c, disable_opt))
+ switch_cases.write(
+ f'{INDENT*4}opts->present.{lower_opt} = true;\n'
+ )
+ switch_cases.write(
+ f'{INDENT*4}opts->enabled.{lower_opt} = false;\n'
+ )
+ switch_cases.write(f'{INDENT*4}break;\n')
+
+ for conflict_opt in option.conflicts:
+ constraint_statements.write(f'''\
+{INDENT}if (HAVE_OPT({upper_opt}) && HAVE_OPT({mangle(conflict_opt).upper()}))
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "the '%s' and '%s' options conflict",
+{INDENT*3} "{long_opt}", "{mangle(conflict_opt)}");
+{INDENT*2}}}
+''')
+ for require_opt in option.requires:
+ constraint_statements.write(f'''\
+{INDENT}if (HAVE_OPT({upper_opt}) && !HAVE_OPT({mangle(require_opt).upper()}))
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "%s option requires the %s options",
+{INDENT*3} "{long_opt}", "{mangle(require_opt)}");
+{INDENT*2}}}
+''')
+ arg_range = option.argument_range
+ if arg_range:
+ constraint_statements.write(f'''\
+{INDENT}if (HAVE_OPT({upper_opt}) && \
+OPT_VALUE_{upper_opt} < {arg_range.minimum})
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "%s option value %d is out of range.",
+{INDENT*3} "{long_opt}", opts->value.{lower_opt});
+{INDENT*2}}}
+''')
+ constraint_statements.write(f'''\
+{INDENT}if (HAVE_OPT({upper_opt}) && \
+OPT_VALUE_{upper_opt} > {arg_range.maximum})
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "%s option value %d is out of range",
+{INDENT*3} "{long_opt}", opts->value.{lower_opt});
+{INDENT*2}}}
+''')
+
+ long_opts.write(f'{INDENT}{{ 0, 0, 0, 0 }}\n')
+
+ switch_cases.write(f'{INDENT*3}default:\n')
+ switch_cases.write(f'{INDENT*4}usage (stderr, EXIT_FAILURE);\n')
+ switch_cases.write(f'{INDENT*4}break;\n')
+
+ argument = desc.tool.argument
+ if argument:
+ if argument.startswith('[') and argument.endswith(']'):
+ argument = argument[1:-1]
+ argument_statement = ''
+ else:
+ argument_statement = f'''\
+{INDENT}if (optind == argc)
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "Command line arguments required");
+{INDENT*2}}}
+'''
+ else:
+ argument_statement = f'''\
+{INDENT}if (optind < argc)
+{INDENT*2}{{
+{INDENT*3}error (EXIT_FAILURE, 0, "Command line arguments are not allowed.");
+{INDENT*2}}}
+'''
+
+ short_opts_concatenated = ''.join(sorted(short_opts))
+ usage_stringified = '\n'.join([
+ f'{INDENT*2}"{line}\\n"' for line in usage(desc, info).split('\n')
+ ])
+ version_v = version(desc, info, 'v')
+ version_c = version(desc, info, 'c')
+ version_n = version(desc, info, 'n')
+ version_v_stringified = '\n'.join([
+ f'{INDENT*6}"{line}\\n"' for line in version_v.split('\n')
+ ])
+ version_c_stringified = '\n'.join([
+ f'{INDENT*6}"{line}\\n"' for line in version_c.split('\n')
+ ])
+ version_n_stringified = '\n'.join([
+ f'{INDENT*6}"{line}\\n"' for line in version_n.split('\n')
+ ])
+
+ if outfile.name.endswith('.c'):
+ outfile_base = outfile.name[:-len('.c')]
+ else:
+ outfile_base = outfile.name
+
+ outfile.write(f'''\
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "{outfile_base}.h"
+#include <errno.h>
+#include <error.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#ifndef _WIN32
+#include <unistd.h>
+#endif /* !_WIN32 */
+#include <limits.h>
+
+struct {struct_name} {global_name};
+
+''')
+
+ if has_list_arg:
+ outfile.write(f'''\
+/* Copied from xsize.h in Gnulib */
+
+/* Sum of two sizes, with overflow check. */
+static inline size_t
+xsum (size_t size1, size_t size2)
+{{
+{INDENT}size_t sum = size1 + size2;
+{INDENT}return (sum >= size1 ? sum : SIZE_MAX);
+}}
+
+/* Check for overflow. */
+#define size_overflow_p(SIZE) \
+{INDENT}((SIZE) == SIZE_MAX)
+
+static void
+append_to_list (struct {mangle(desc.tool.name)}_list *list,
+ const char *name, const char *arg)
+{{
+{INDENT}const char **tmp;
+{INDENT}size_t new_count = xsum (list->count, 1);
+
+{INDENT}if (size_overflow_p (new_count))
+{INDENT*2}error (EXIT_FAILURE, 0, "too many arguments for %s",
+{INDENT*2} name);
+
+{INDENT}tmp = reallocarray (list->args, new_count, sizeof (char *));
+{INDENT}if (!tmp)
+{INDENT*2}error (EXIT_FAILURE, 0, "unable to allocate memory for %s",
+{INDENT*2} name);
+
+{INDENT}list->args = tmp;
+{INDENT}list->args[list->count] = optarg;
+{INDENT}list->count = new_count;
+}}
+
+''')
+
+ if has_number_arg:
+ outfile.write(f'''\
+static long
+parse_number (const char *arg)
+{{
+{INDENT}char *endptr = NULL;
+{INDENT}errno = 0;
+{INDENT}long result;
+
+{INDENT}if (strncmp (arg, "0x", 2) == 0)
+{INDENT*2}result = strtol (arg + 2, &endptr, 16);
+{INDENT}else if (strncmp (arg, "0", 1) == 0
+{INDENT} && strspn (arg, "012345678") == strlen (optarg))
+{INDENT*2}result = strtol (arg + 1, &endptr, 8);
+{INDENT}else
+{INDENT*2}result = strtol (arg, &endptr, 10);
+
+{INDENT}if (errno != 0 || (endptr && *endptr != '\\0'))
+{INDENT*2}error (EXIT_FAILURE, errno, "'%s' is not a recognizable number.",
+{INDENT*2} arg);
+
+{INDENT}return result;
+}}
+
+''')
+
+ outfile.write(f'''\
+/* Long options. */
+static const struct option long_options[] =
+{{
+{long_opts.getvalue()}
+}};
+
+''')
+ if has_default_arg:
+ outfile.write(f'''\
+/* Default options. */
+{default_definitions.getvalue().rstrip()}
+
+''')
+ outfile.write(f'''\
+int
+process_options (int argc, char **argv)
+{{
+{INDENT}struct {struct_name} *opts = &{global_name};
+{INDENT}int opt;
+
+''')
+ if has_default_arg:
+ outfile.write(f'''\
+{default_statements.getvalue().rstrip()}
+''')
+ outfile.write(f'''\
+{enable_statements.getvalue().rstrip()}
+{INDENT}while ((opt = getopt_long (argc, argv, "{short_opts_concatenated}",
+{INDENT} long_options, NULL)) != EOF)
+{INDENT*2}switch (opt)
+{INDENT*3}{{
+{switch_cases.getvalue().rstrip()}
+{INDENT*3}}}
+
+{constraint_statements.getvalue().rstrip()}
+{argument_statement}
+
+{INDENT}if (HAVE_OPT(HELP))
+{INDENT*2}{{
+{INDENT*3}USAGE(0);
+{INDENT*2}}}
+
+{INDENT}if (HAVE_OPT(MORE_HELP))
+#ifdef _WIN32
+{INDENT*2}{{
+{INDENT*3}USAGE(0);
+{INDENT*2}}}
+#else /* _WIN32 */
+{INDENT*2}{{
+{INDENT*3}pid_t pid;
+{INDENT*3}int pfds[2];
+
+{INDENT*3}if (pipe (pfds) < 0)
+{INDENT*4}error (EXIT_FAILURE, errno, "pipe");
+
+{INDENT*3}pid = fork ();
+{INDENT*3}if (pid < 0)
+{INDENT*4}error (EXIT_FAILURE, errno, "fork");
+
+{INDENT*3}if (pid == 0)
+{INDENT*4}{{
+{INDENT*5}close (pfds[0]);
+{INDENT*5}dup2 (pfds[1], STDOUT_FILENO);
+{INDENT*5}close (pfds[1]);
+
+{INDENT*5}usage (stdout, 0);
+{INDENT*4}}}
+{INDENT*3}else
+{INDENT*4}{{
+{INDENT*5}const char *args[2];
+{INDENT*5}const char *envvar;
+
+{INDENT*5}close (pfds[1]);
+{INDENT*5}dup2 (pfds[0], STDIN_FILENO);
+{INDENT*5}close (pfds[0]);
+
+{INDENT*5}envvar = secure_getenv ("PAGER");
+{INDENT*5}if (!envvar || *envvar == '\\0')
+{INDENT*6}args[0] = "more";
+{INDENT*5}else
+{INDENT*6}args[0] = envvar;
+
+{INDENT*5}args[1] = NULL;
+
+{INDENT*5}execvp (args[0], (char * const *)args);
+
+{INDENT*5}exit (EXIT_FAILURE);
+{INDENT*4}}}
+{INDENT*2}}}
+#endif /* !_WIN32 */
+
+{INDENT}if (HAVE_OPT(VERSION))
+{INDENT*2}{{
+{INDENT*3}if (!OPT_ARG_VERSION || !strcmp (OPT_ARG_VERSION, "c"))
+{INDENT*4}{{
+{INDENT*5}const char str[] =
+{version_c_stringified};
+{INDENT*5}fprintf (stdout, "%s", str);
+{INDENT*5}exit(0);
+{INDENT*4}}}
+{INDENT*3}else if (!strcmp (OPT_ARG_VERSION, "v"))
+{INDENT*4}{{
+{INDENT*5}const char str[] =
+{version_v_stringified};
+{INDENT*5}fprintf (stdout, "%s", str);
+{INDENT*5}exit(0);
+{INDENT*4}}}
+{INDENT*3}else if (!strcmp (OPT_ARG_VERSION, "n"))
+{INDENT*4}{{
+{INDENT*5}const char str[] =
+{version_n_stringified};
+{INDENT*5}fprintf (stdout, "%s", str);
+{INDENT*5}exit(0);
+{INDENT*4}}}
+{INDENT*3}else
+{INDENT*4}{{
+{INDENT*5}error (EXIT_FAILURE, 0,
+{INDENT*5} "version option argument 'a' invalid. Use:\\n"
+{INDENT*5} " 'v' - version only\\n"
+{INDENT*5} " 'c' - version and copyright\\n"
+{INDENT*5} " 'n' - version and full copyright notice");
+{INDENT*4}}}
+{INDENT*2}}}
+
+{INDENT}return optind;
+}}
+
+void
+usage (FILE *out, int status)
+{{
+{INDENT}const char str[] =
+{usage_stringified};
+{INDENT}fprintf (out, "%s", str);
+{INDENT}exit (status);
+}}
+''')
+
+
+def generate_header(desc: Desc, info: Info, outfile: TextIO):
+ struct_members_present = io.StringIO()
+ struct_members_arg = io.StringIO()
+ struct_members_value = io.StringIO()
+ struct_members_enabled = io.StringIO()
+ struct_members_list = io.StringIO()
+ have_opts = io.StringIO()
+ opt_args = io.StringIO()
+ opt_values = io.StringIO()
+ enabled_opts = io.StringIO()
+ opts_count = io.StringIO()
+ opts_array = io.StringIO()
+ has_list_arg = False
+
+ struct_name = f'{mangle(desc.tool.name)}_options'
+ global_name = f'{mangle(desc.tool.name)}_options'
+ list_struct_name = f'{mangle(desc.tool.name)}_list'
+
+ options = [option for section in desc.sections
+ for option in section.options]
+
+ for option in options:
+ long_opt = option.long_option
+ arg_type = option.argument_type
+ lower_opt = mangle(long_opt)
+ upper_opt = lower_opt.upper()
+ lower_opt = escape_c_keyword(lower_opt)
+
+ # aliases are handled differently
+ if option.aliases:
+ continue
+
+ struct_members_present.write(f'{INDENT*2}bool {lower_opt};\n')
+
+ if arg_type:
+ if option.multiple:
+ has_list_arg = True
+ struct_members_list.write(
+ f'{INDENT*2}struct {list_struct_name} {lower_opt};\n'
+ )
+ opts_count.write((
+ f'#define OPTS_COUNT_{upper_opt} '
+ f'{global_name}.list.{lower_opt}.count\n'
+ ))
+ opts_array.write((
+ f'#define OPTS_ARRAY_{upper_opt} '
+ f'{global_name}.list.{lower_opt}.args\n'
+ ))
+ else:
+ struct_members_arg.write(
+ f'{INDENT*2}const char *{lower_opt};\n'
+ )
+ if arg_type == ArgumentType.NUMBER:
+ struct_members_value.write(f'{INDENT*2}int {lower_opt};\n')
+ opt_values.write((
+ f'#define OPT_VALUE_{upper_opt} '
+ f'{global_name}.value.{lower_opt}\n'
+ ))
+
+ struct_members_enabled.write(f'{INDENT*2}bool {lower_opt};\n')
+ enabled_opts.write((
+ f'#define ENABLED_OPT_{upper_opt} '
+ f'{global_name}.enabled.{lower_opt}\n'
+ ))
+
+ have_opts.write((
+ f'#define HAVE_OPT_{upper_opt} '
+ f'{global_name}.present.{lower_opt}\n'
+ ))
+ opt_args.write((
+ f'#define OPT_ARG_{upper_opt} '
+ f'{global_name}.arg.{lower_opt}\n'
+ ))
+
+ header_guard = f'{mangle(outfile.name).upper()}_'
+
+ outfile.write(f'''\
+#include <stdbool.h>
+#include <stdio.h>
+
+#ifndef {header_guard}
+#define {header_guard} 1
+
+struct {list_struct_name}
+{{
+{INDENT}const char **args;
+{INDENT}unsigned int count;
+}};
+
+struct {struct_name}
+{{
+{INDENT}/* Options present in the command line */
+{INDENT}struct
+{INDENT}{{
+{struct_members_present.getvalue().rstrip()}
+{INDENT}}} present;
+
+{INDENT}/* Option arguments in raw string form */
+{INDENT}struct
+{INDENT}{{
+{struct_members_arg.getvalue().rstrip()}
+{INDENT}}} arg;
+
+{INDENT}/* Option arguments parsed as integer */
+{INDENT}struct
+{INDENT}{{
+{struct_members_value.getvalue().rstrip()}
+{INDENT}}} value;
+''')
+ if has_list_arg:
+ outfile.write(f'''
+{INDENT}/* Option arguments parsed as list */
+{INDENT}struct
+{INDENT}{{
+{struct_members_list.getvalue().rstrip()}
+{INDENT}}} list;
+''')
+ outfile.write(f'''
+{INDENT}/* Option enablement status */
+{INDENT}struct
+{INDENT}{{
+{struct_members_enabled.getvalue().rstrip()}
+{INDENT}}} enabled;
+}};
+
+#define HAVE_OPT(name) HAVE_OPT_ ## name
+#define OPT_ARG(name) OPT_ARG_ ## name
+#define ENABLED_OPT(name) ENABLED_OPT_ ## name
+#define OPTS_COUNT(name) OPTS_COUNT_ ## name
+#define OPTS_ARRAY(name) OPTS_ARRAY_ ## name
+#define USAGE(status) usage (stdout, (status))
+
+{have_opts.getvalue()}
+{opt_args.getvalue()}
+{opt_values.getvalue()}
+{enabled_opts.getvalue()}
+{opts_count.getvalue()}
+{opts_array.getvalue()}
+
+extern struct {struct_name} {global_name};
+int process_options (int argc, char **argv);
+void usage (FILE *out, int status);
+
+#endif /* {header_guard} */
+''')
diff --git a/cligen/cligen/doc/__init__.py b/cligen/cligen/doc/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cligen/cligen/doc/__init__.py
diff --git a/cligen/cligen/doc/man.py b/cligen/cligen/doc/man.py
new file mode 100644
index 0000000..c631bf2
--- /dev/null
+++ b/cligen/cligen/doc/man.py
@@ -0,0 +1,273 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from typing import Mapping, Optional, TextIO, Sequence
+import datetime
+import io
+import re
+from cligen.types import ArgumentType, Desc, ToolDesc, OptionDesc
+from cligen.package import Info, license
+
+
+def generate_options(tool: ToolDesc,
+ options: Sequence[OptionDesc]) -> str:
+ docs = io.StringIO()
+ for option in options:
+ long_opt = option.long_option
+ long_opt_escaped = long_opt.replace('-', '\\-')
+ short_opt = option.short_option
+ desc = option.description
+ disable_prefix = option.disable_prefix
+ if disable_prefix:
+ disable_opt: Optional[str] = f'{disable_prefix}{long_opt}'
+ else:
+ disable_opt = None
+ if option.aliases:
+ docs.write(f'''\
+.TP
+.NOP \\f\\*[B-Font]\\-\\-{long_opt_escaped}\\f[]
+This is an alias for the \\fI--{option.aliases}\\fR option.
+''')
+ if option.deprecated:
+ docs.write('''\
+.sp
+.B
+NOTE: THIS OPTION IS DEPRECATED
+''')
+ continue
+
+ if option.argument_type:
+ if option.argument_name:
+ arg_name = option.argument_name.lower()
+ else:
+ arg_name = option.argument_type.get_name()
+ arg = f'\\f\\*[I-Font]{arg_name}\\f[]'
+ long_arg = f'={arg}'
+ short_arg = f' {arg}'
+ else:
+ long_arg = ''
+ short_arg = ''
+ formatted_options = list()
+ if short_opt:
+ formatted_options.append(
+ f'\\f\\*[B-Font]\\-{short_opt}\\f[]{short_arg}'
+ )
+ formatted_options.append(
+ f'\\f\\*[B-Font]\\-\\-{long_opt_escaped}\\f[]{long_arg}'
+ )
+ if disable_opt:
+ disable_opt_escaped = disable_opt.replace('-', '\\-')
+ formatted_options.append(
+ f'\\f\\*[B-Font]\\-\\-{disable_opt_escaped}\\f[]'
+ )
+ docs.write(f'''\
+.TP
+.NOP {', '.join(formatted_options)}
+''')
+ if desc and desc[0].isupper():
+ docs.write(f'{desc}.\n')
+ if option.multiple:
+ docs.write(
+ 'This option may appear an unlimited number of times.\n'
+ )
+ if option.argument_type == ArgumentType.NUMBER:
+ docs.write(
+ 'This option takes an integer number as its argument.\n'
+ )
+ if option.argument_range:
+ docs.write(f'''\
+The value of
+\\f\\*[I-Font]{arg_name}\\f[]
+is constrained to being:
+.in +4
+.nf
+.na
+in the range {option.argument_range.minimum} through \
+{option.argument_range.maximum}
+.fi
+.in -4
+''')
+ if option.argument_default:
+ docs.write(f'''\
+The default
+\\f\\*[I-Font]number\\f[]
+for this option is:
+.ti +4
+ {option.argument_default}
+.sp
+''')
+ if len(option.conflicts) > 0:
+ docs.write(f'''\
+This option must not appear in combination with any of the following options:
+{', '.join(option.conflicts)}.
+''')
+ if len(option.requires) > 0:
+ docs.write(f'''\
+This option must appear in combination with the following options:
+{', '.join(option.requires)}.
+''')
+ if disable_opt:
+ disable_opt_escaped = disable_opt.replace('-', '\\-')
+ docs.write((
+ f'The \\fI{disable_opt_escaped}\\fP form '
+ 'will disable the option.\n'
+ ))
+ if option.enabled:
+ docs.write('This option is enabled by default.\n')
+ if desc and desc[0].isupper():
+ docs.write('.sp\n')
+ if option.detail:
+ docs.write(f'{text_to_man(option.detail)}\n')
+ if option.deprecated:
+ docs.write('''\
+.sp
+.B
+NOTE: THIS OPTION IS DEPRECATED
+''')
+ return docs.getvalue()
+
+
+def text_to_man(s: str) -> str:
+ s = re.sub(r'-', r'\\-', s)
+ s = re.sub(r'(?m)^$', r'.sp', s)
+ s = re.sub(r"``(.*)''", r'\\(lq\1\\(rq', s)
+ return s
+
+
+def texi_to_man(s: str) -> str:
+ s = text_to_man(s)
+ s = re.sub(r'@([{}@])', r'\1', s)
+ s = re.sub(r'@code\{(.*?)\}', r'\\fB\1\\fP', s)
+ s = re.sub(r'@file\{(.*?)\}', r'\\fI\1\\fP', s)
+ s = re.sub(r'@subheading (.*)', r'''.br
+\\fB\1\\fP
+.br''', s)
+ s = re.sub(r'@example', r'''.br
+.in +4
+.nf''', s)
+ s = re.sub(r'@end example', r'''.in -4
+.fi''', s)
+ return s
+
+
+def include(name: str, includes: Mapping[str, TextIO]) -> str:
+ docs = io.StringIO()
+ f = includes.get(name)
+ if f:
+ docs.write(texi_to_man(f.read().strip()))
+ return docs.getvalue()
+
+
+def generate(desc: Desc, info: Info,
+ includes: Mapping[str, TextIO],
+ outfile: TextIO):
+ description = includes.get('description')
+ if description:
+ detail = texi_to_man(description.read())
+ elif desc.tool.detail:
+ detail = desc.tool.detail
+ else:
+ detail = ''
+
+ section_docs = io.StringIO()
+ for section in desc.sections:
+ if section.ref:
+ option_docs = generate_options(desc.tool, section.options)
+ section_docs.write(f'''\
+.SS "{section.description}"
+{option_docs}\
+''')
+ else:
+ section_docs.write(generate_options(desc.tool, section.options))
+
+ formatted_date = datetime.date.today().strftime('%d %b %Y')
+ detail_concatenated = '\n.sp\n'.join(detail.strip().split('\n\n'))
+ outfile.write(f'''\
+.de1 NOP
+. it 1 an-trap
+. if \\\\n[.$] \\,\\\\$*\\/
+..
+.ie t \\
+.ds B-Font [CB]
+.ds I-Font [CI]
+.ds R-Font [CR]
+.el \\
+.ds B-Font B
+.ds I-Font I
+.ds R-Font R
+.TH {desc.tool.name} 1 "{formatted_date}" "{info.version}" "User Commands"
+.SH NAME
+\\f\\*[B-Font]{desc.tool.name}\\fP
+\\- {desc.tool.title}
+.SH SYNOPSIS
+\\f\\*[B-Font]{desc.tool.name}\\fP
+.\\" Mixture of short (flag) options and long options
+[\\f\\*[B-Font]\\-flags\\f[]]
+[\\f\\*[B-Font]\\-flag\\f[] [\\f\\*[I-Font]value\\f[]]]
+[\\f\\*[B-Font]\\-\\-option-name\\f[][[=| ]\\f\\*[I-Font]value\\f[]]]
+''')
+ if desc.tool.argument:
+ outfile.write(f'''\
+{desc.tool.argument}
+.sp \\n(Ppu
+.ne 2
+
+Operands and options may be intermixed. They will be reordered.
+.sp \\n(Ppu
+.ne 2
+''')
+ else:
+ outfile.write('''\
+.sp \\n(Ppu
+.ne 2
+
+All arguments must be options.
+.sp \\n(Ppu
+.ne 2
+''')
+ outfile.write(f'''\
+.SH "DESCRIPTION"
+{detail_concatenated}
+.sp
+.SH "OPTIONS"
+{section_docs.getvalue()}
+''')
+ if 'files' in includes:
+ outfile.write(f'''\
+.SH FILES
+{include('files', includes)}
+''')
+ if 'examples' in includes:
+ outfile.write(f'''\
+.sp
+.SH EXAMPLES
+{include('examples', includes)}
+''')
+ outfile.write('''\
+.SH "EXIT STATUS"
+One of the following exit values will be returned:
+.TP
+.NOP 0 " (EXIT_SUCCESS)"
+Successful program execution.
+.TP
+.NOP 1 " (EXIT_FAILURE)"
+The operation failed or the command syntax was not valid.
+.PP
+''')
+ if 'see-also' in includes:
+ outfile.write(f'''\
+.SH "SEE ALSO"
+{include('see-also', includes)}
+''')
+ outfile.write(f'''\
+.SH "AUTHORS"
+{', '.join(info.authors)}
+.SH "COPYRIGHT"
+Copyright (C) {info.copyright_year} {info.copyright_holder}
+{license(desc, info, 'brief')}.
+''')
+ if info.bug_email:
+ outfile.write(f'''\
+.SH "BUGS"
+Please send bug reports to: {info.bug_email}
+''')
diff --git a/cligen/cligen/doc/texi.py b/cligen/cligen/doc/texi.py
new file mode 100644
index 0000000..3121d52
--- /dev/null
+++ b/cligen/cligen/doc/texi.py
@@ -0,0 +1,218 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from typing import Mapping, Sequence, TextIO
+import io
+import cligen.code
+from cligen.types import Desc, ToolDesc, OptionDesc
+from cligen.package import Info
+
+
+HEADINGS = ['@heading', '@subheading', '@subsubheading']
+
+
+def get_heading(level: int) -> str:
+ return HEADINGS[min(level, len(HEADINGS)-1)]
+
+
+SECTIONS = ['@section', '@subsection', '@subsubsection']
+
+
+def get_section(level: int) -> str:
+ return SECTIONS[min(level, len(SECTIONS)-1)]
+
+
+def shift_headings(s: str, level: int) -> str:
+ for (i, h) in reversed(list(enumerate(HEADINGS))):
+ r = get_heading(level+i)
+ s = s.replace(h, r)
+ return s
+
+
+def generate_options(tool: ToolDesc,
+ options: Sequence[OptionDesc],
+ level: int) -> str:
+ docs = io.StringIO()
+ for option in options:
+ if option.aliases:
+ docs.write(f'''\
+{get_heading(level+1)} {option.long_option} option.
+@anchor{{{tool.name} {option.long_option}}}
+
+This is an alias for the @code{{{option.aliases}}} option,
+@pxref{{{tool.name} {option.aliases}, \
+the {option.aliases} option documentation}}.
+
+''')
+ continue
+
+ if not option.detail or not option.description:
+ continue
+ if option.short_option:
+ docs.write(
+ f'{get_heading(level+1)} {option.long_option} option '
+ f'(-{option.short_option}).\n'
+ )
+ else:
+ docs.write(
+ f'{get_heading(level+1)} {option.long_option} option.\n'
+ )
+ docs.write(f'''\
+@anchor{{{tool.name} {option.long_option}}}
+
+This is the ``{option.description.lower()}'' option.
+''')
+ if option.argument_type:
+ if option.argument_name:
+ docs.write((
+ f'This option takes a {option.argument_type} argument '
+ f'@file{{{option.argument_name}}}.\n'
+ ))
+ else:
+ docs.write(f'This option takes '
+ f'a {option.argument_type} argument.\n')
+
+ if len(option.conflicts) > 0 or len(option.requires) > 0 or \
+ option.enabled:
+ docs.write('''
+@noindent
+This option has some usage constraints. It:
+@itemize @bullet
+''')
+ if len(option.conflicts) > 0:
+ docs.write(f'''\
+@item
+must not appear in combination with any of the following options:
+{', '.join(option.conflicts)}.
+''')
+ if len(option.requires) > 0:
+ docs.write(f'''\
+@item
+must appear in combination with the following options:
+{', '.join(option.requires)}.
+''')
+ if option.disable_prefix:
+ docs.write(f'''\
+@item
+can be disabled with --{option.disable_prefix}{option.long_option}.
+''')
+ if option.enabled:
+ docs.write('''\
+@item
+It is enabled by default.
+''')
+ docs.write('@end itemize\n\n')
+
+ docs.write(f'''\
+{option.detail}
+''')
+ if option.deprecated:
+ docs.write('\n@strong{NOTE}@strong{: THIS OPTION IS DEPRECATED}\n')
+
+ return docs.getvalue()
+
+
+LABELS = {
+ 'see-also': 'See Also',
+ 'examples': 'Examples',
+ 'files': 'Files'
+}
+
+
+def include(tool: ToolDesc,
+ name: str,
+ includes: Mapping[str, TextIO],
+ level: int) -> str:
+ docs = io.StringIO()
+ f = includes.get(name)
+ if f:
+ docs.write(f'''\
+@anchor{{{tool.name} {LABELS[name]}}}
+{get_heading(level+2)} {tool.name} {LABELS[name]}
+{shift_headings(f.read(), level)}\
+''')
+ return docs.getvalue()
+
+
+def escape_texi(s: str) -> str:
+ for c in ['@', '{', '}']:
+ s = s.replace(c, f'@{c}')
+ return s
+
+
+def generate(desc: Desc, info: Info,
+ includes: Mapping[str, TextIO],
+ outfile: TextIO,
+ level: int = 0,
+ section_node: bool = True):
+ description = includes.get('description')
+ if description:
+ detail = description.read()
+ elif desc.tool.detail:
+ detail = desc.tool.detail
+ else:
+ detail = ''
+
+ section_docs = io.StringIO()
+ for section in desc.sections:
+ if section.ref:
+ option_docs = generate_options(desc.tool,
+ section.options,
+ level+1)
+ assert section.description
+ section_docs.write(f'''\
+@anchor{{{desc.tool.name} {section.ref}}}
+{get_heading(level+1)} {section.ref} options
+{section.description.strip('.')}.
+{option_docs}\
+''')
+ else:
+ section_docs.write(generate_options(desc.tool,
+ section.options,
+ level))
+
+ heading = get_section(level) if section_node else get_heading(level)
+ outfile.write(f'''\
+@node {desc.tool.name} Invocation
+{heading} Invoking {desc.tool.name}
+@pindex {desc.tool.name}
+
+{detail}
+
+@anchor{{{desc.tool.name} usage}}
+{get_heading(level+1)} {desc.tool.name} help/usage (@option{{-?}})
+@cindex {desc.tool.name} help
+
+The text printed is the same whether selected with the @code{{help}} option
+(@option{{--help}}) or the @code{{more-help}} option \
+(@option{{--more-help}}). @code{{more-help}} will print
+the usage text by passing it through a pager program.
+@code{{more-help}} is disabled on platforms without a working
+@code{{fork(2)}} function. The @code{{PAGER}} environment variable is
+used to select the program, defaulting to @file{{more}}. Both will exit
+with a status code of 0.
+
+@exampleindent 0
+@example
+{escape_texi(cligen.code.usage(desc, info))}
+@end example
+@exampleindent 4
+
+{section_docs.getvalue()}\
+@anchor{{{desc.tool.name} exit status}}
+{get_heading(level+1)} {desc.tool.name} exit status
+
+One of the following exit values will be returned:
+@table @samp
+@item 0 (EXIT_SUCCESS)
+Successful program execution.
+@item 1 (EXIT_FAILURE)
+The operation failed or the command syntax was not valid.
+@end table
+''')
+ if 'see-also' in includes:
+ outfile.write(include(desc.tool, 'see-also', includes, level))
+ if 'examples' in includes:
+ outfile.write(include(desc.tool, 'examples', includes, level))
+ if 'files' in includes:
+ outfile.write(include(desc.tool, 'files', includes, level))
diff --git a/cligen/cligen/package.py b/cligen/cligen/package.py
new file mode 100644
index 0000000..75adf14
--- /dev/null
+++ b/cligen/cligen/package.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from typing import NamedTuple, Optional, Sequence
+from cligen.types import Desc
+import datetime
+import io
+import os
+import textwrap
+
+
+try:
+ import pwd
+
+ def get_default_copyright_holder():
+ return pwd.getpwuid(os.getuid()).pw_gecos
+except ImportError:
+ def get_default_copyright_holder():
+ return 'COPYRIGHT HOLDER'
+
+
+class Info(NamedTuple):
+ name: str
+ version: str
+ license: str = 'gpl3+'
+ copyright_year: str = str(datetime.date.today().year)
+ copyright_holder: str = get_default_copyright_holder()
+ bug_email: Optional[str] = None
+ authors: Sequence[str] = list()
+
+
+BRIEF_LICENSES = {
+ 'gpl3+': textwrap.dedent('''\
+ This program is released under the terms of
+ the GNU General Public License, version 3 or later
+ ''')
+}
+SHORT_LICENSES = {
+ 'gpl3+': textwrap.dedent('''\
+ This is free software. It is licensed for use, modification and
+ redistribution under the terms of the GNU General Public License,
+ version 3 or later <http://gnu.org/licenses/gpl.html>
+ ''')
+}
+FULL_LICENSES = {
+ 'gpl3+': textwrap.dedent('''\
+ This is free software. It is licensed for use, modification and
+ redistribution under the terms of the GNU General Public License,
+ version 3 or later <http://gnu.org/licenses/gpl.html>
+
+ @PACKAGE_NAME@ 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 3 of the License, or (at your option) any later version.
+
+ @PACKAGE_NAME@ 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. If not, see <http://www.gnu.org/licenses/>.
+ ''')
+}
+
+
+def license(desc: Desc, info: Info, what='short') -> str:
+ if what == 'brief':
+ license_text = BRIEF_LICENSES[info.license]
+ elif what == 'short':
+ license_text = SHORT_LICENSES[info.license]
+ elif what == 'full':
+ license_text = FULL_LICENSES[info.license]
+ return license_text.replace('@PACKAGE_NAME@', info.name)
+
+
+def version(desc: Desc, info: Info, what='c') -> str:
+ out = io.StringIO()
+
+ if what == 'v':
+ out.write(f'{desc.tool.name} {info.version}')
+ elif what == 'c':
+ out.write(textwrap.dedent(f'''\
+ {desc.tool.name} {info.version}
+ Copyright (C) {info.copyright_year} {info.copyright_holder}
+ '''))
+ out.write(license(desc, info, 'short'))
+ if info.bug_email:
+ out.write(textwrap.dedent(f'''\
+
+ Please send bug reports to: <{info.bug_email}>\
+ '''))
+ elif what == 'n':
+ out.write(textwrap.dedent(f'''\
+ {desc.tool.name} {info.version}
+ Copyright (C) {info.copyright_year} {info.copyright_holder}
+ '''))
+ out.write(license(desc, info, 'full'))
+ if info.bug_email:
+ out.write(textwrap.dedent(f'''\
+
+ Please send bug reports to: <{info.bug_email}>\
+ '''))
+ return out.getvalue()
diff --git a/cligen/cligen/types.py b/cligen/cligen/types.py
new file mode 100644
index 0000000..9f3d304
--- /dev/null
+++ b/cligen/cligen/types.py
@@ -0,0 +1,183 @@
+# Copyright (C) 2021-2022 Daiki Ueno
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from typing import NamedTuple, Optional, Sequence, Union
+from enum import Enum, auto
+import textwrap
+
+
+class ToolDesc(NamedTuple):
+ name: str
+ title: str
+ description: str
+ short_usage: str
+ detail: Optional[str] = None
+ argument: Optional[str] = None
+ reorder_arguments: bool = False
+
+ @classmethod
+ def from_json(cls, obj):
+ return cls(name=obj['name'],
+ title=obj['title'],
+ description=obj['description'],
+ detail=obj['detail'],
+ short_usage=obj['short-usage'],
+ argument=obj.get('argument'),
+ reorder_arguments=obj.get('reorder-arguments', False))
+
+
+class ArgumentType(Enum):
+ STRING = auto()
+ NUMBER = auto()
+ FILE = auto()
+ KEYWORD = auto()
+
+ @classmethod
+ def from_json(cls, obj):
+ return cls.__members__[obj.upper()]
+
+ def get_name(self) -> str:
+ ARG_TYPE_TO_VALUE = {
+ ArgumentType.STRING: 'str',
+ ArgumentType.NUMBER: 'num',
+ ArgumentType.FILE: 'file',
+ ArgumentType.KEYWORD: 'arg',
+ }
+ return ARG_TYPE_TO_VALUE[self]
+
+
+class Range(NamedTuple):
+ minimum: Optional[int]
+ maximum: Optional[int]
+
+ @classmethod
+ def from_json(cls, obj):
+ default = cls.default()
+ return cls(minimum=obj.get('min', default.minimum),
+ maximum=obj.get('max', default.maximum))
+
+
+class SignedRange(Range):
+ @classmethod
+ def default(cls):
+ return cls(minimum=-2**31-1, maximum=2**31)
+
+
+class UnsignedRange(Range):
+ @classmethod
+ def default(cls):
+ return cls(minimum=0, maximum=2**32)
+
+
+class OptionDesc(NamedTuple):
+ long_option: str
+ short_option: Optional[str] = None
+ description: Optional[str] = None
+ detail: Optional[str] = None
+ argument_optional: bool = False
+ file_exists: bool = False
+ deprecated: bool = False
+ aliases: Optional[str] = None
+ conflicts: Sequence[str] = list()
+ requires: Sequence[str] = list()
+ argument_range: Optional[Range] = None
+ argument_type: Optional[ArgumentType] = None
+ argument_name: Optional[str] = None
+ argument_default: Optional[Union[str, int]] = None
+ multiple: bool = False
+ occur_range: Optional[Range] = None
+ enabled: bool = False
+ disable_prefix: Optional[str] = None
+ enable_prefix: Optional[str] = None
+
+ @classmethod
+ def from_json(cls, obj):
+ return cls(long_option=obj['long-option'],
+ short_option=obj.get('short-option'),
+ description=obj.get('description'),
+ detail=obj.get('detail'),
+ argument_optional=obj.get('argument-optional', False),
+ file_exists=obj.get('file-exists', False),
+ deprecated=obj.get('deprecated', False),
+ aliases=obj.get('aliases'),
+ conflicts=obj.get('conflicts', list()),
+ requires=obj.get('requires', list()),
+ argument_range=SignedRange.from_json(
+ obj['argument-range']
+ ) if 'argument-range' in obj else None,
+ argument_type=ArgumentType.from_json(
+ obj['argument-type']
+ ) if 'argument-type' in obj else None,
+ argument_name=obj.get('argument-name'),
+ argument_default=obj.get('argument-default'),
+ multiple=obj.get('multiple'),
+ occur_range=UnsignedRange.from_json(
+ obj['occurrences']
+ ) if 'occurrences' in obj else None,
+ enabled=obj.get('enabled', False),
+ disable_prefix=obj.get('disable-prefix'),
+ enable_prefix=obj.get('enable-prefix'))
+
+
+class SectionDesc(NamedTuple):
+ ref: Optional[str] = None
+ description: Optional[str] = None
+ options: Sequence[OptionDesc] = list()
+
+ @classmethod
+ def from_json(cls, obj):
+ return cls(ref=obj.get('ref'),
+ description=obj.get('description'),
+ options=[OptionDesc.from_json(option)
+ for option in obj['options']])
+
+ @classmethod
+ def default(cls):
+ return DEFAULT_SECTION
+
+
+class Desc(NamedTuple):
+ tool: ToolDesc
+ sections: Sequence[SectionDesc] = list()
+
+ @classmethod
+ def from_json(cls, obj):
+ return cls(tool=ToolDesc.from_json(obj['tool']),
+ sections=[
+ SectionDesc.from_json(section)
+ for section in obj['sections']
+ ] + [
+ SectionDesc.default()
+ ])
+
+
+DEFAULT_SECTION = SectionDesc(
+ description='Version, usage and configuration options',
+ options=[
+ OptionDesc(
+ long_option='version',
+ short_option='v',
+ argument_type=ArgumentType.KEYWORD,
+ argument_optional=True,
+ description='output version information and exit',
+ detail=textwrap.fill(textwrap.dedent("""\
+ Output version of program and exit.
+ The default mode is `v', a simple version.
+ The `c' mode will print copyright information and
+ `n' will print the full copyright notice.\
+ """), width=72, fix_sentence_endings=True)
+ ),
+ OptionDesc(
+ long_option='help',
+ short_option='h',
+ description='display extended usage information and exit',
+ detail='Display usage information and exit.'
+ ),
+ OptionDesc(
+ long_option='more-help',
+ short_option='!',
+ description='extended usage information passed thru pager',
+ detail='Pass the extended usage information through a pager.'
+ )
+ ]
+)