diff options
Diffstat (limited to 'cligen')
-rwxr-xr-x | cligen/cli-codegen.py | 40 | ||||
-rwxr-xr-x | cligen/cli-docgen.py | 57 | ||||
-rw-r--r-- | cligen/cligen.mk | 9 | ||||
-rw-r--r-- | cligen/cligen/__init__.py | 0 | ||||
-rw-r--r-- | cligen/cligen/code.py | 749 | ||||
-rw-r--r-- | cligen/cligen/doc/__init__.py | 0 | ||||
-rw-r--r-- | cligen/cligen/doc/man.py | 273 | ||||
-rw-r--r-- | cligen/cligen/doc/texi.py | 218 | ||||
-rw-r--r-- | cligen/cligen/package.py | 104 | ||||
-rw-r--r-- | cligen/cligen/types.py | 183 |
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.' + ) + ] +) |