summaryrefslogtreecommitdiffstats
path: root/lib/ansiblelint/cli.py
blob: 6b395615dc000f2985e2bfdd1021c6bed375861a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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: