Completed
Push — master ( bff2ab...5dc771 )
by Satoru
01:07
created

anyconfig._output_result()   C

Complexity

Conditions 7

Size

Total Lines 25

Duplication

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