From 49419a0bd85188753160fd7e7b64f2f26cd896b1 Mon Sep 17 00:00:00 2001 From: Philippe Proulx Date: Fri, 7 Aug 2020 17:17:13 -0400 Subject: [PATCH] cli: add `show-effective-configuration` command This new command is equivalent to `generate --dump-config`, but in a more dedicated way: $ barectf show-effective-configuration config.yaml It only accepts relevant options. One of the options is `--indent-spaces` which controls the indentation space count of printed YAML lines. This patch deprecates `--dump-config`: I'll leave it working in barectf 3 to remain backward-compatible, but I'm removing it from the `generate` command's help. Signed-off-by: Philippe Proulx --- barectf/cli.py | 419 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 276 insertions(+), 143 deletions(-) diff --git a/barectf/cli.py b/barectf/cli.py index 956ee0c..468879e 100644 --- a/barectf/cli.py +++ b/barectf/cli.py @@ -22,6 +22,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import pkg_resources +import collections import termcolor import argparse import os.path @@ -104,78 +105,66 @@ def _opt_item_val(items, long_name, default=None): return True -class _CliCfg: +class _CliError(Exception): pass -class _CliGenCmdCfg(_CliCfg): - def __init__(self, config_file_path, c_source_dir, c_header_dir, metadata_stream_dir, - inclusion_dirs, ignore_inclusion_not_found, dump_config, v2_prefix): - self._config_file_path = config_file_path - self._c_source_dir = c_source_dir - self._c_header_dir = c_header_dir - self._metadata_stream_dir = metadata_stream_dir - self._inclusion_dirs = inclusion_dirs - self._ignore_inclusion_not_found = ignore_inclusion_not_found - self._dump_config = dump_config - self._v2_prefix = v2_prefix +# Returns a `_CfgCmdCfg` object from the command-line parsing results +# `parse_res`. +def _cfg_cmd_cfg_from_parse_res(parse_res): + # check configuration file path + cfg_file_path = None - @property - def config_file_path(self): - return self._config_file_path + for item in parse_res.items: + if type(item) is barectf_argpar._NonOptItem: + if cfg_file_path is not None: + raise _CliError('Multiple configuration file paths provided') - @property - def c_source_dir(self): - return self._c_source_dir + cfg_file_path = item.text - @property - def c_header_dir(self): - return self._c_header_dir + if cfg_file_path is None: + raise _CliError('Missing configuration file path') - @property - def metadata_stream_dir(self): - return self._metadata_stream_dir + if not os.path.isfile(cfg_file_path): + raise _CliError(f'`{cfg_file_path}` is not an existing, regular file') - @property - def inclusion_dirs(self): - return self._inclusion_dirs + # inclusion directories + inclusion_dirs = [item.arg_text for item in _find_opt_items(parse_res.items, 'include-dir')] - @property - def ignore_inclusion_not_found(self): - return self._ignore_inclusion_not_found + for dir in inclusion_dirs: + if not os.path.isdir(dir): + raise _CliError(f'`{dir}` is not an existing directory') - @property - def dump_config(self): - return self._dump_config + inclusion_dirs.append(os.getcwd()) - @property - def v2_prefix(self): - return self._v2_prefix + # other options + ignore_inclusion_file_not_found = _opt_item_val(parse_res.items, 'ignore-include-not-found', + False) + + return _CfgCmdCfg(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found) def _print_gen_cmd_usage(): print('''Usage: barectf generate [--code-dir=DIR] [--headers-dir=DIR] [--metadata-dir=DIR] [--prefix=PREFIX] [--include-dir=DIR]... [--ignore-include-not-found] - [--dump-config] CONFIG-FILE-PATH + CONFIG-FILE-PATH Options: - -c DIR, --code-dir=DIR Write C source files to DIR - --dump-config Print the effective configuration file - -H DIR, --headers-dir=DIR Write C header files to DIR + -c DIR, --code-dir=DIR Write C source files to DIR instead of the CWD + -H DIR, --headers-dir=DIR Write C header files to DIR instead of the CWD --ignore-include-not-found Continue to process the configuration file when included files are not found -I DIR, --include-dir=DIR Add DIR to the list of directories to be searched for inclusion files - -m DIR, --metadata-dir=DIR Write the metadata stream file to DIR + -m DIR, --metadata-dir=DIR Write the metadata stream file to DIR instead of + the CWD -p PREFIX, --prefix=PREFIX Set the configuration prefix to PREFIX''') -class _CliError(Exception): - pass - - -def _cli_gen_cfg_from_args(orig_args): +# Returns a source and metadata stream file generating command object +# from the specific command-line arguments `orig_args`. +def _gen_cmd_cfg_from_args(orig_args): # parse original arguments opt_descrs = [ barectf_argpar.OptDescr('h', 'help'), @@ -195,41 +184,77 @@ def _cli_gen_cfg_from_args(orig_args): _print_gen_cmd_usage() sys.exit() - # check configuration file path - config_file_path = None - - for item in res.items: - if type(item) is barectf_argpar._NonOptItem: - if config_file_path is not None: - raise _CliError('Multiple configuration file paths provided') - - config_file_path = item.text - - if config_file_path is None: - raise _CliError('Missing configuration file path') - - if not os.path.isfile(config_file_path): - raise _CliError(f'`{config_file_path}` is not an existing, regular file') + # get common configuration file command CLI configuration + cfg_cmd_cfg = _cfg_cmd_cfg_from_parse_res(res) # directories c_source_dir = _opt_item_val(res.items, 'code-dir', os.getcwd()) c_header_dir = _opt_item_val(res.items, 'headers-dir', os.getcwd()) metadata_stream_dir = _opt_item_val(res.items, 'metadata-dir', os.getcwd()) - inclusion_dirs = [item.arg_text for item in _find_opt_items(res.items, 'include-dir')] - for dir in [c_source_dir, c_header_dir, metadata_stream_dir] + inclusion_dirs: + for dir in [c_source_dir, c_header_dir, metadata_stream_dir]: if not os.path.isdir(dir): raise _CliError(f'`{dir}` is not an existing directory') - inclusion_dirs.append(os.getcwd()) - # other options - ignore_inclusion_not_found = _opt_item_val(res.items, 'ignore-include-not-found', False) dump_config = _opt_item_val(res.items, 'dump-config', False) v2_prefix = _opt_item_val(res.items, 'prefix') - return _CliGenCmdCfg(config_file_path, c_source_dir, c_header_dir, metadata_stream_dir, - inclusion_dirs, ignore_inclusion_not_found, dump_config, v2_prefix) + return _GenCmd(_GenCmdCfg(cfg_cmd_cfg.cfg_file_path, c_source_dir, c_header_dir, + metadata_stream_dir, cfg_cmd_cfg.inclusion_dirs, + cfg_cmd_cfg.ignore_inclusion_file_not_found, dump_config, v2_prefix)) + + +def _print_show_effective_cfg_cmd_usage(): + print('''Usage: barectf show-effective-configuration [--include-dir=DIR]... + [--ignore-include-not-found] + [--indent-spaces=COUNT] CONFIG-FILE-PATH + +Options: + --ignore-include-not-found Continue to process the configuration file when + included files are not found + -I DIR, --include-dir=DIR Add DIR to the list of directories to be + searched for inclusion files + --indent-spaces=COUNT Use COUNT spaces at a time to indent YAML lines + instead of 2''') + + +# Returns an effective configuration showing command object from the +# specific command-line arguments `orig_args`. +def _show_effective_cfg_cfg_from_args(orig_args): + # parse original arguments + opt_descrs = [ + barectf_argpar.OptDescr('h', 'help'), + barectf_argpar.OptDescr('I', 'include-dir', True), + barectf_argpar.OptDescr(long_name='indent-spaces', has_arg=True), + barectf_argpar.OptDescr(long_name='ignore-include-not-found'), + ] + res = barectf_argpar.parse(orig_args, opt_descrs) + assert len(res.ingested_orig_args) == len(orig_args) + + # command help? + if len(_find_opt_items(res.items, 'help')) > 0: + _print_show_effective_cfg_cmd_usage() + sys.exit() + + # get common configuration command CLI configuration + cfg_cmd_cfg = _cfg_cmd_cfg_from_parse_res(res) + + # other options + indent_space_count = _opt_item_val(res.items, 'indent-spaces', 2) + + try: + indent_space_count = int(indent_space_count) + except (ValueError, TypeError): + raise _CliError(f'Invalid `--indent-spaces` option argument: `{indent_space_count}`') + + if indent_space_count < 1 or indent_space_count > 8: + raise _CliError(f'Invalid `--indent-spaces` option argument (`{indent_space_count}`): expecting a value in [1, 8]') + + return _ShowEffectiveCfgCmd(_ShowEffectiveCfgCmdCfg(cfg_cmd_cfg.cfg_file_path, + cfg_cmd_cfg.inclusion_dirs, + cfg_cmd_cfg.ignore_inclusion_file_not_found, + indent_space_count)) def _print_general_usage(): @@ -242,13 +267,19 @@ General options: -V, --version Show version and quit Available commands: - gen, generate Generate the C source and CTF metadata files of a tracer - from a configuration file + gen, generate Generate the C source and CTF metadata files + of a tracer from a configuration file + show-effective-configuration, Print the effective configuration file for a + show-effective-config given configuration file and inclusion + directories Run `barectf COMMAND --help` to show the help of COMMAND.''') -def _cli_cfg_from_args(): +# Returns a command object from the command-line arguments `orig_args`. +# +# All the `orig_args` elements are considered. +def _cmd_from_args(orig_args): # We use our `argpar` module here instead of Python's `argparse` # because we need to support the two following use cases: # @@ -262,21 +293,28 @@ def _cli_cfg_from_args(): barectf_argpar.OptDescr('V', 'version'), barectf_argpar.OptDescr('h', 'help'), ] - orig_args = sys.argv[1:] res = barectf_argpar.parse(orig_args, general_opt_descrs, False) # find command name, collecting preceding (common) option items + cmd_from_args_funcs = { + 'generate': _gen_cmd_cfg_from_args, + 'gen': _gen_cmd_cfg_from_args, + 'show-effective-configuration': _show_effective_cfg_cfg_from_args, + 'show-effective-config': _show_effective_cfg_cfg_from_args, + 'show-effective-cfg': _show_effective_cfg_cfg_from_args, + } general_opt_items = [] cmd_first_orig_arg_index = None - cmd_name = None + cmd_from_args_func = None for item in res.items: if type(item) is barectf_argpar._NonOptItem: - if item.text in ['gen', 'generate']: - cmd_name = 'generate' - cmd_first_orig_arg_index = item.orig_arg_index + 1 - else: + cmd_from_args_func = cmd_from_args_funcs.get(item.text) + + if cmd_from_args_func is None: cmd_first_orig_arg_index = item.orig_arg_index + else: + cmd_first_orig_arg_index = item.orig_arg_index + 1 break else: @@ -296,80 +334,175 @@ def _cli_cfg_from_args(): # execute command cmd_orig_args = orig_args[cmd_first_orig_arg_index:] - if cmd_name is None: + if cmd_from_args_func is None: # default `generate` command - return _cli_gen_cfg_from_args(cmd_orig_args) + return _gen_cmd_cfg_from_args(cmd_orig_args) else: - assert cmd_name == 'generate' - return _cli_gen_cfg_from_args(cmd_orig_args) + return cmd_from_args_func(cmd_orig_args) + + +class _CmdCfg: + pass + + +class _CfgCmdCfg(_CmdCfg): + def __init__(self, cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found): + self._cfg_file_path = cfg_file_path + self._inclusion_dirs = inclusion_dirs + self._ignore_inclusion_file_not_found = ignore_inclusion_file_not_found + + @property + def cfg_file_path(self): + return self._cfg_file_path + + @property + def inclusion_dirs(self): + return self._inclusion_dirs + + @property + def ignore_inclusion_file_not_found(self): + return self._ignore_inclusion_file_not_found + + +class _Cmd: + def __init__(self, cfg): + self._cfg = cfg + + @property + def cfg(self): + return self._cfg + + def exec(self): + raise NotImplementedError + + +class _GenCmdCfg(_CfgCmdCfg): + def __init__(self, cfg_file_path, c_source_dir, c_header_dir, metadata_stream_dir, + inclusion_dirs, ignore_inclusion_file_not_found, dump_config, v2_prefix): + super().__init__(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found) + self._c_source_dir = c_source_dir + self._c_header_dir = c_header_dir + self._metadata_stream_dir = metadata_stream_dir + self._dump_config = dump_config + self._v2_prefix = v2_prefix + + @property + def c_source_dir(self): + return self._c_source_dir + + @property + def c_header_dir(self): + return self._c_header_dir + + @property + def metadata_stream_dir(self): + return self._metadata_stream_dir + + @property + def dump_config(self): + return self._dump_config + + @property + def v2_prefix(self): + return self._v2_prefix + + +# Source and metadata stream file generating command. +class _GenCmd(_Cmd): + def exec(self): + # create configuration + try: + with open(self.cfg.cfg_file_path) as f: + if self.cfg.dump_config: + # print effective configuration file + print(barectf.effective_configuration_file(f, True, self.cfg.inclusion_dirs, + self.cfg.ignore_inclusion_file_not_found)) + + # barectf.configuration_from_file() reads the file again + # below: rewind. + f.seek(0) + + config = barectf.configuration_from_file(f, True, self.cfg.inclusion_dirs, + self.cfg.ignore_inclusion_file_not_found) + except barectf._ConfigurationParseError as exc: + _print_config_error(exc) + except Exception as exc: + _print_unknown_exc(exc) + + if self.cfg.v2_prefix is not None: + # Override prefixes. + # + # For historical reasons, the `--prefix` option applies the + # barectf 2 configuration prefix rules. Therefore, get the + # equivalent barectf 3 prefixes first. + v3_prefixes = barectf_config_parse_common._v3_prefixes_from_v2_prefix(self.cfg.v2_prefix) + cg_opts = config.options.code_generation_options + cg_opts = barectf.ConfigurationCodeGenerationOptions(v3_prefixes.identifier, + v3_prefixes.file_name, + cg_opts.default_stream_type, + cg_opts.header_options, + cg_opts.clock_type_c_types) + config = barectf.Configuration(config.trace, barectf.ConfigurationOptions(cg_opts)) + + # create a barectf code generator + code_gen = barectf.CodeGenerator(config) + + def write_file(dir, file): + with open(os.path.join(dir, file.name), 'w') as f: + f.write(file.contents) + + def write_files(dir, files): + for file in files: + write_file(dir, file) + + try: + # generate and write metadata stream file + write_file(self.cfg.metadata_stream_dir, code_gen.generate_metadata_stream()) + + # generate and write C header files + write_files(self.cfg.c_header_dir, code_gen.generate_c_headers()) + + # generate and write C source files + write_files(self.cfg.c_source_dir, code_gen.generate_c_sources()) + except Exception as exc: + # We know `config` is valid, therefore the code generator cannot + # fail for a reason known to barectf. + _print_unknown_exc(exc) + + +class _ShowEffectiveCfgCmdCfg(_CfgCmdCfg): + def __init__(self, cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found, + indent_space_count): + super().__init__(cfg_file_path, inclusion_dirs, ignore_inclusion_file_not_found) + self._indent_space_count = indent_space_count + + @property + def indent_space_count(self): + return self._indent_space_count + + +# Effective configuration showing command. +class _ShowEffectiveCfgCmd(_Cmd): + def exec(self): + try: + with open(self.cfg.cfg_file_path) as f: + print(barectf.effective_configuration_file(f, True, self.cfg.inclusion_dirs, + self.cfg.ignore_inclusion_file_not_found, + self.cfg.indent_space_count)) + except barectf._ConfigurationParseError as exc: + _print_config_error(exc) + except Exception as exc: + _print_unknown_exc(exc) def _run(): - # parse arguments + # create command from arguments try: - cli_cfg = _cli_cfg_from_args() + cmd = _cmd_from_args(sys.argv[1:]) except barectf_argpar._Error as exc: _print_error(f'Command-line: For argument `{exc.orig_arg}`: {exc.msg}') except _CliError as exc: _print_error(f'Command-line: {exc}') - assert type(cli_cfg) is _CliGenCmdCfg - - # create configuration - try: - with open(cli_cfg.config_file_path) as f: - if cli_cfg.dump_config: - # print effective configuration file - print(barectf.effective_configuration_file(f, True, cli_cfg.inclusion_dirs, - cli_cfg.ignore_inclusion_not_found)) - - # barectf.configuration_from_file() reads the file again - # below: rewind. - f.seek(0) - - config = barectf.configuration_from_file(f, True, cli_cfg.inclusion_dirs, - cli_cfg.ignore_inclusion_not_found) - except barectf._ConfigurationParseError as exc: - _print_config_error(exc) - except Exception as exc: - _print_unknown_exc(exc) - - if cli_cfg.v2_prefix is not None: - # Override prefixes. - # - # For historical reasons, the `--prefix` option applies the - # barectf 2 configuration prefix rules. Therefore, get the - # equivalent barectf 3 prefixes first. - v3_prefixes = barectf_config_parse_common._v3_prefixes_from_v2_prefix(cli_cfg.v2_prefix) - cg_opts = config.options.code_generation_options - cg_opts = barectf.ConfigurationCodeGenerationOptions(v3_prefixes.identifier, - v3_prefixes.file_name, - cg_opts.default_stream_type, - cg_opts.header_options, - cg_opts.clock_type_c_types) - config = barectf.Configuration(config.trace, barectf.ConfigurationOptions(cg_opts)) - - # create a barectf code generator - code_gen = barectf.CodeGenerator(config) - - def write_file(dir, file): - with open(os.path.join(dir, file.name), 'w') as f: - f.write(file.contents) - - def write_files(dir, files): - for file in files: - write_file(dir, file) - - try: - # generate and write metadata stream file - write_file(cli_cfg.metadata_stream_dir, code_gen.generate_metadata_stream()) - - # generate and write C header files - write_files(cli_cfg.c_header_dir, code_gen.generate_c_headers()) - - # generate and write C source files - write_files(cli_cfg.c_source_dir, code_gen.generate_c_sources()) - except Exception as exc: - # We know `config` is valid, therefore the code generator cannot - # fail for a reason known to barectf. - _print_unknown_exc(exc) + # execute command + cmd.exec() -- 2.34.1