Completed
Push — master ( 613b11...22be07 )
by Satoru
01:00
created

_output_result()   C

Complexity

Conditions 7

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
dl 0
loc 24
rs 5.5
c 1
b 0
f 0
1
#
2
# Author: Satoru SATOH <ssato redhat.com>
3
# License: MIT
4
#
5
"""CLI frontend module for anyconfig.
6
"""
7
from __future__ import absolute_import, print_function
8
9
import codecs
10
import locale
11
import logging
12
import optparse
13
import os
14
import sys
15
16
import anyconfig.api as API
17
import anyconfig.compat
18
import anyconfig.globals
19
import anyconfig.mdicts
20
import anyconfig.parser
21
22
23
_ENCODING = locale.getdefaultlocale()[1]
24
25
LOGGER = logging.getLogger("anyconfig")
26
LOGGER.addHandler(logging.StreamHandler())
27
LOGGER.setLevel(logging.WARN)
28
29
if anyconfig.compat.IS_PYTHON_3:
30
    import io
31
32
    _ENCODING = _ENCODING.lower()
33
34
    # TODO: What should be done for an error, "AttributeError: '_io.StringIO'
35
    # object has no attribute 'buffer'"?
36
    try:
37
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=_ENCODING)
38
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=_ENCODING)
39
    except AttributeError:
40
        pass
41
else:
42
    sys.stdout = codecs.getwriter(_ENCODING)(sys.stdout)
43
    sys.stderr = codecs.getwriter(_ENCODING)(sys.stderr)
44
45
USAGE = """\
46
%prog [Options...] CONF_PATH_OR_PATTERN_0 [CONF_PATH_OR_PATTERN_1 ..]
47
48
Examples:
49
  %prog --list  # -> Supported config types: configobj, ini, json, ...
50
  # Merge and/or convert input config to output config [file]
51
  %prog -I yaml -O yaml /etc/xyz/conf.d/a.conf
52
  %prog -I yaml '/etc/xyz/conf.d/*.conf' -o xyz.conf --otype json
53
  %prog '/etc/xyz/conf.d/*.json' -o xyz.yml \\
54
    --atype json -A '{"obsoletes": "syscnf", "conflicts": "syscnf-old"}'
55
  %prog '/etc/xyz/conf.d/*.json' -o xyz.yml \\
56
    -A obsoletes:syscnf;conflicts:syscnf-old
57
  %prog /etc/foo.json /etc/foo/conf.d/x.json /etc/foo/conf.d/y.json
58
  %prog '/etc/foo.d/*.json' -M noreplace
59
  # Get/set part of input config
60
  %prog '/etc/foo.d/*.json' --get a.b.c
61
  %prog '/etc/foo.d/*.json' --set a.b.c=1
62
  # Validate with JSON schema or generate JSON schema:
63
  %prog --validate -S foo.conf.schema.yml '/etc/foo.d/*.xml'
64
  %prog --gen-schema '/etc/foo.d/*.xml' -o foo.conf.schema.yml"""
65
66
DEFAULTS = dict(loglevel=1, list=False, output=None, itype=None,
67
                otype=None, atype=None, merge=API.MS_DICTS,
68
                ignore_missing=False, template=False, env=False,
69
                schema=None, validate=False, gen_schema=False)
70
71
72
def to_log_level(level):
73
    """
74
    :param level: Logging level in int = 0 .. 2
75
76
    >>> to_log_level(0) == logging.WARN
77
    True
78
    >>> to_log_level(5)  # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
79
    Traceback (most recent call last):
80
        ...
81
    ValueError: wrong log level passed: 5
82
    >>>
83
    """
84
    if not (level >= 0 and level < 3):
85
        raise ValueError("wrong log level passed: " + str(level))
86
87
    return [logging.WARN, logging.INFO, logging.DEBUG][level]
88
89
90
_ATYPE_HELP_FMT = """\
91
Explicitly select type of argument to provide configs from %s.
92
93
If this option is not set, original parser is used: 'K:V' will become {K: V},
94
'K:V_0,V_1,..' will become {K: [V_0, V_1, ...]}, and 'K_0:V_0;K_1:V_1' will
95
become {K_0: V_0, K_1: V_1} (where the tyep of K is str, type of V is one of
96
Int, str, etc."""
97
98
_GET_HELP = ("Specify key path to get part of config, for example, "
99
             "'--get a.b.c' to config {'a': {'b': {'c': 0, 'd': 1}}} "
100
             "gives 0 and '--get a.b' to the same config gives "
101
             "{'c': 0, 'd': 1}. Path expression can be JSON Pointer "
102
             "expression (http://tools.ietf.org/html/rfc6901) such like "
103
             "'', '/a~1b', '/m~0n'.")
104
_SET_HELP = ("Specify key path to set (update) part of config, for "
105
             "example, '--set a.b.c=1' to a config {'a': {'b': {'c': 0, "
106
             "'d': 1}}} gives {'a': {'b': {'c': 1, 'd': 1}}}.")
107
108
109
def parse_args(argv=None, defaults=None):
110
    """
111
    Make up an option and arguments parser.
112
113
    :param defaults: Default option values
114
    """
115
    if defaults is None:
116
        defaults = DEFAULTS
117
118
    ctypes = API.list_types()
119
    ctypes_s = ", ".join(ctypes)
120
    type_help = "Select type of %s config files from " + \
121
        ctypes_s + " [Automatically detected by file ext]"
122
123
    mts = API.MERGE_STRATEGIES
124
    mts_s = ", ".join(mts)
125
    mt_help = "Select strategy to merge multiple configs from " + \
126
        mts_s + " [%(merge)s]" % defaults
127
128
    parser = optparse.OptionParser(USAGE, version="%%prog %s" %
129
                                   anyconfig.globals.VERSION)
130
    parser.set_defaults(**defaults)
131
132
    lpog = optparse.OptionGroup(parser, "List specific options")
133
    lpog.add_option("-L", "--list", help="List supported config types",
134
                    action="store_true")
135
    parser.add_option_group(lpog)
136
137
    spog = optparse.OptionGroup(parser, "Schema specific options")
138
    spog.add_option("", "--validate", action="store_true",
139
                    help="Only validate input files and do not output. "
140
                         "You must specify schema file with -S/--schema "
141
                         "option.")
142
    spog.add_option("", "--gen-schema", action="store_true",
143
                    help="Generate JSON schema for givne config file[s] "
144
                         "and output it instead of (merged) configuration.")
145
    parser.add_option_group(spog)
146
147
    gspog = optparse.OptionGroup(parser, "Get/set options")
148
    gspog.add_option("", "--get", help=_GET_HELP)
149
    gspog.add_option("", "--set", help=_SET_HELP)
150
    parser.add_option_group(gspog)
151
152
    parser.add_option("-o", "--output", help="Output file path")
153
    parser.add_option("-I", "--itype", choices=ctypes,
154
                      help=(type_help % "Input"))
155
    parser.add_option("-O", "--otype", choices=ctypes,
156
                      help=(type_help % "Output"))
157
    parser.add_option("-M", "--merge", choices=mts, help=mt_help)
158
    parser.add_option("-A", "--args", help="Argument configs to override")
159
    parser.add_option("", "--atype", choices=ctypes,
160
                      help=_ATYPE_HELP_FMT % ctypes_s)
161
162
    parser.add_option("-x", "--ignore-missing", action="store_true",
163
                      help="Ignore missing input files")
164
    parser.add_option("-T", "--template", action="store_true",
165
                      help="Enable template config support")
166
    parser.add_option("-E", "--env", action="store_true",
167
                      help="Load configuration defaults from "
168
                           "environment values")
169
    parser.add_option("-S", "--schema", help="Specify Schema file[s] path")
170
    parser.add_option("-s", "--silent", action="store_const", dest="loglevel",
171
                      const=0, help="Silent or quiet mode")
172
    parser.add_option("-q", "--quiet", action="store_const", dest="loglevel",
173
                      const=0, help="Same as --silent option")
174
    parser.add_option("-v", "--verbose", action="store_const", dest="loglevel",
175
                      const=2, help="Verbose mode")
176
177
    if argv is None:
178
        argv = sys.argv
179
180
    (options, args) = parser.parse_args(argv[1:])
181
    return (parser, options, args)
182
183
184
def _exit_with_output(content, exit_code=0):
185
    """
186
    Exit the program with printing out messages.
187
188
    :param content: content to print out
189
    :param exit_code: Exit code
190
    """
191
    (sys.stdout if exit_code == 0 else sys.stderr).write(content + "\n")
192
    sys.exit(exit_code)
193
194
195
def _check_options_and_args(parser, options, args):
196
    """
197
    Show supported config format types or usage.
198
199
    :param parser: Option parser object
200
    :param options: Options optparse.OptionParser.parse_args returns
201
    :param args: Arguments optparse.OptionParser.parse_args returns
202
    """
203
    if not args:
204
        if options.list:
205
            tlist = ", ".join(API.list_types())
206
            _exit_with_output("Supported config types: " + tlist)
207
        else:
208
            parser.print_usage()
209
            sys.exit(1)
210
211
    if options.validate and options.schema is None:
212
        _exit_with_output("--validate option requires --scheme option", 1)
213
214
215
def _exit_if_load_failure(cnf, msg):
216
    """
217
    :param cnf: Loaded configuration object or None indicates load failure
218
    :param msg: Message to print out if failure
219
    """
220
    if cnf is None:
221
        _exit_with_output(msg, 1)
222
223
224
def _exit_if_only_to_validate(only_to_validate):
225
    """
226
    :param only_to_validate: True if it's only to validate
227
    """
228
    if only_to_validate:
229
        _exit_with_output("Validation succeds")
230
231
232
def _do_get(cnf, get_path):
233
    """
234
    :param cnf: Configuration object to print out
235
    :param get_path: key path given in --get option
236
    :return: updated Configuration object if no error
237
    """
238
    (cnf, err) = API.get(cnf, get_path)
239
    if cnf is None:  # Failed to get the result.
240
        _exit_with_output("Failed to get result: err=%s" % err, 1)
241
242
    return cnf
243
244
245
def _output_type_by_input_path(inpath, itype, fmsg):
246
    """
247
    :param inpath: Input file path
248
    :param itype: Input type or None
249
    :param fmsg: message if it cannot detect otype by `inpath`
250
    :return: Output type :: str
251
    """
252
    msg = ("Specify inpath and/or outpath type[s] with -I/--itype "
253
           "or -O/--otype option explicitly")
254
    if itype is None:
255
        try:
256
            otype = API.find_loader(inpath).type()
257
        except API.UnknownFileTypeError:
258
            _exit_with_output((fmsg % inpath) + msg, 1)
259
        except ValueError:
260
            _exit_with_output(msg, 1)
261
    else:
262
        otype = itype
263
264
    return otype
265
266
267
def _output_result(cnf, outpath, otype, inpath, itype):
268
    """
269
    :param cnf: Configuration object to print out
270
    :param outpath: Output file path or None
271
    :param otype: Output type or None
272
    :param inpath: Input file path
273
    :param itype: Input type or None
274
    """
275
    fmsg = ("Uknown file type and cannot detect appropriate backend "
276
            "from its extension, '%s'")
277
    if not outpath or outpath == "-":
278
        outpath = sys.stdout
279
        if otype is None:
280
            otype = _output_type_by_input_path(inpath, itype, fmsg)
281
282
    if anyconfig.mdicts.is_dict_like(cnf):
283
        try:
284
            API.dump(cnf, outpath, otype)
285
        except API.UnknownFileTypeError:
286
            _exit_with_output(fmsg % outpath, 1)
287
        except API.UnknownParserTypeError:
288
            _exit_with_output("Invalid output type '%s'" % otype, 1)
289
    else:
290
        _exit_with_output(str(cnf))  # Output primitive types as it is.
291
292
293
def main(argv=None):
294
    """
295
    :param argv: Argument list to parse or None (sys.argv will be set).
296
    """
297
    (parser, options, args) = parse_args(argv=argv)
298
    LOGGER.setLevel(to_log_level(options.loglevel))
299
300
    _check_options_and_args(parser, options, args)
301
302
    cnf = API.to_container(os.environ.copy() if options.env else {})
303
    try:
304
        diff = API.load(args, options.itype,
305
                        ignore_missing=options.ignore_missing,
306
                        ac_merge=options.merge,
307
                        ac_template=options.template,
308
                        ac_schema=options.schema)
309
    except API.UnknownParserTypeError:
310
        _exit_with_output("Wrong input type '%s'" % options.itype, 1)
311
    except API.UnknownFileTypeError:
312
        _exit_with_output("No appropriate backend was found for given file "
313
                          "'%s'" % options.itype, 1)
314
    _exit_if_load_failure(diff, "Failed to load: args=%s" % ", ".join(args))
315
    cnf.update(diff)
316
317
    if options.args:
318
        diff = anyconfig.parser.parse(options.args)
319
        cnf.update(diff)
320
321
    _exit_if_only_to_validate(options.validate)
322
323
    if options.gen_schema:
324
        cnf = API.gen_schema(cnf)
325
326
    if options.get:
327
        cnf = _do_get(cnf, options.get)
328
329
    if options.set:
330
        (key, val) = options.set.split('=')
331
        API.set_(cnf, key, anyconfig.parser.parse(val))
332
333
    _output_result(cnf, options.output, options.otype, args[0], options.itype)
334
335
336
if __name__ == '__main__':
337
    main(sys.argv)
338
339
# vim:sw=4:ts=4:et:
340