# -*- coding: utf-8 -*- """CLI parser setup and helpers.""" import argparse import logging import os import sys from pathlib import Path from typing import List, NamedTuple import yaml from ansiblelint.constants import DEFAULT_RULESDIR, INVALID_CONFIG_RC from ansiblelint.utils import expand_path_vars from ansiblelint.version import __version__ _logger = logging.getLogger(__name__) _PATH_VARS = ['exclude_paths', 'rulesdir', ] def abspath(path: str, base_dir: str) -> str: """Make relative path absolute relative to given directory. Args: path (str): the path to make absolute base_dir (str): the directory from which make relative paths absolute default_drive: Windows drive to use to make the path absolute if none is given. """ if not os.path.isabs(path): # Don't use abspath as it assumes path is relative to cwd. # We want it relative to base_dir. path = os.path.join(base_dir, path) return os.path.normpath(path) def expand_to_normalized_paths(config: dict, base_dir: str = None) -> None: # config can be None (-c /dev/null) if not config: return base_dir = base_dir or os.getcwd() for paths_var in _PATH_VARS: if paths_var not in config: continue # Cause we don't want to add a variable not present normalized_paths = [] for path in config.pop(paths_var): normalized_path = abspath(expand_path_vars(path), base_dir=base_dir) normalized_paths.append(normalized_path) config[paths_var] = normalized_paths def load_config(config_file: str) -> dict: config_path = os.path.abspath(config_file or '.ansible-lint') if config_file: if not os.path.exists(config_path): _logger.error("Config file not found '%s'", config_path) sys.exit(INVALID_CONFIG_RC) elif not os.path.exists(config_path): # a missing default config file should not trigger an error return {} try: with open(config_path, "r") as stream: config = yaml.safe_load(stream) except yaml.YAMLError as e: _logger.error(e) sys.exit(INVALID_CONFIG_RC) # TODO(ssbarnea): implement schema validation for config file if isinstance(config, list): _logger.error( "Invalid configuration '%s', expected YAML mapping in the config file.", config_path) sys.exit(INVALID_CONFIG_RC) config_dir = os.path.dirname(config_path) expand_to_normalized_paths(config, config_dir) return config class AbspathArgAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): if isinstance(values, (str, Path)): values = [values] normalized_values = [Path(expand_path_vars(path)).resolve() for path in values] previous_values = getattr(namespace, self.dest, []) setattr(namespace, self.dest, previous_values + normalized_values) def get_cli_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument('-L', dest='listrules', default=False, action='store_true', help="list all the rules") parser.add_argument('-f', dest='format', default='rich', choices=['rich', 'plain', 'rst'], help="Format used rules output, (default: %(default)s)") parser.add_argument('-q', dest='quiet', default=False, action='store_true', help="quieter, although not silent output") parser.add_argument('-p', dest='parseable', default=False, action='store_true', help="parseable output in the format of pep8") parser.add_argument('--parseable-severity', dest='parseable_severity', default=False, action='store_true', help="parseable output including severity of rule") parser.add_argument('--progressive', dest='progressive', default=False, action='store_true', help="Return success if it detects a reduction in number" " of violations compared with previous git commit. This " "feature works only in git repositories.") parser.add_argument('-r', action=AbspathArgAction, dest='rulesdir', default=[], type=Path, help="Specify custom rule directories. Add -R " f"to keep using embedded rules from {DEFAULT_RULESDIR}") parser.add_argument('-R', action='store_true', default=False, dest='use_default_rules', help="Keep default rules when using -r") parser.add_argument('--show-relpath', dest='display_relative_path', action='store_false', default=True, help="Display path relative to CWD") parser.add_argument('-t', dest='tags', action='append', default=[], help="only check rules whose id/tags match these values") parser.add_argument('-T', dest='listtags', action='store_true', help="list all the tags") parser.add_argument('-v', dest='verbosity', action='count', help="Increase verbosity level", default=0) parser.add_argument('-x', dest='skip_list', default=[], action='append', help="only check rules whose id/tags do not " "match these values") parser.add_argument('-w', dest='warn_list', default=[], action='append', help="only warn about these rules, unless overridden in " "config file defaults to 'experimental'") parser.add_argument('--nocolor', dest='colored', default=hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(), action='store_false', help="disable colored output") parser.add_argument('--force-color', dest='colored', action='store_true', help="Try force colored output (relying on ansible's code)") parser.add_argument('--exclude', dest='exclude_paths', action=AbspathArgAction, type=Path, default=[], help='path to directories or files to skip. ' 'This option is repeatable.', ) parser.add_argument('-c', dest='config_file', help='Specify configuration file to use. ' 'Defaults to ".ansible-lint"') parser.add_argument('--version', action='version', version='%(prog)s {ver!s}'.format(ver=__version__), ) parser.add_argument(dest='playbook', nargs='*', help="One or more files or paths. When missing it will " " enable auto-detection mode.") return parser def merge_config(file_config, cli_config) -> NamedTuple: bools = ( 'display_relative_path', 'parseable', 'parseable_severity', 'quiet', 'use_default_rules', ) # maps lists to their default config values lists_map = { 'exclude_paths': [], 'rulesdir': [], 'skip_list': [], 'tags': [], 'warn_list': ['experimental'], } if not file_config: return cli_config for entry in bools: x = getattr(cli_config, entry) or file_config.get(entry, False) setattr(cli_config, entry, x) for entry, default in lists_map.items(): getattr(cli_config, entry).extend(file_config.get(entry, default)) if 'verbosity' in file_config: cli_config.verbosity = (cli_config.verbosity + file_config['verbosity']) return cli_config def get_config(arguments: List[str]): parser = get_cli_parser() options = parser.parse_args(arguments) config = load_config(options.config_file) return merge_config(config, options) def print_help(file=sys.stdout): get_cli_parser().print_help(file=file) # vim: et:sw=4:syntax=python:ts=4: