diff options
Diffstat (limited to 'lib/ansiblelint/cli.py')
-rw-r--r-- | lib/ansiblelint/cli.py | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/lib/ansiblelint/cli.py b/lib/ansiblelint/cli.py new file mode 100644 index 0000000..6b39561 --- /dev/null +++ b/lib/ansiblelint/cli.py @@ -0,0 +1,219 @@ +# -*- 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: |