Completed
Branch master (146025)
by Satoru
01:04
created

main()   B

Complexity

Conditions 6

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
dl 0
loc 30
rs 7.5384
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 _try_dump(cnf, outpath, otype, fmsg):
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 fmsg: message if it cannot detect otype by `inpath`
273
    """
274
    try:
275
        API.dump(cnf, outpath, otype)
276
    except API.UnknownFileTypeError:
277
        _exit_with_output(fmsg % outpath, 1)
278
    except API.UnknownParserTypeError:
279
        _exit_with_output("Invalid output type '%s'" % otype, 1)
280
281
282
def _output_result(cnf, outpath, otype, inpath, itype):
283
    """
284
    :param cnf: Configuration object to print out
285
    :param outpath: Output file path or None
286
    :param otype: Output type or None
287
    :param inpath: Input file path
288
    :param itype: Input type or None
289
    """
290
    fmsg = ("Uknown file type and cannot detect appropriate backend "
291
            "from its extension, '%s'")
292
293
    if not anyconfig.mdicts.is_dict_like(cnf):
294
        _exit_with_output(str(cnf))  # Print primitive types as it is.
295
296
    if not outpath or outpath == "-":
297
        outpath = sys.stdout
298
        if otype is None:
299
            otype = _output_type_by_input_path(inpath, itype, fmsg)
300
301
    _try_dump(cnf, outpath, otype, fmsg)
302
303
304
def _load_diff(args, options):
305
    """
306
    :param args: Extra argument list
307
    :param options: :class:`optparse.Values` object
308
    """
309
    try:
310
        diff = API.load(args, options.itype,
311
                        ignore_missing=options.ignore_missing,
312
                        ac_merge=options.merge,
313
                        ac_template=options.template,
314
                        ac_schema=options.schema)
315
    except API.UnknownParserTypeError:
316
        _exit_with_output("Wrong input type '%s'" % options.itype, 1)
317
    except API.UnknownFileTypeError:
318
        _exit_with_output("No appropriate backend was found for given file "
319
                          "'%s'" % options.itype, 1)
320
    _exit_if_load_failure(diff, "Failed to load: args=%s" % ", ".join(args))
321
322
    return diff
323
324
325
def main(argv=None):
326
    """
327
    :param argv: Argument list to parse or None (sys.argv will be set).
328
    """
329
    (parser, options, args) = parse_args(argv=argv)
330
    LOGGER.setLevel(to_log_level(options.loglevel))
331
332
    _check_options_and_args(parser, options, args)
333
334
    cnf = API.to_container(os.environ.copy() if options.env else {})
335
    diff = _load_diff(args, options)
336
    cnf.update(diff)
337
338
    if options.args:
339
        diff = anyconfig.parser.parse(options.args)
340
        cnf.update(diff)
341
342
    _exit_if_only_to_validate(options.validate)
343
344
    if options.gen_schema:
345
        cnf = API.gen_schema(cnf)
346
347
    if options.get:
348
        cnf = _do_get(cnf, options.get)
349
350
    if options.set:
351
        (key, val) = options.set.split('=')
352
        API.set_(cnf, key, anyconfig.parser.parse(val))
353
354
    _output_result(cnf, options.output, options.otype, args[0], options.itype)
355
356
357
if __name__ == '__main__':
358
    main(sys.argv)
359
360
# vim:sw=4:ts=4:et:
361