ansible-later/testenv/lib/python2.7/site-packages/anyconfig/cli.py

371 lines
13 KiB
Python
Raw Normal View History

2019-04-23 11:04:27 +00:00
#
# Author: Satoru SATOH <ssato redhat.com>
# License: MIT
#
"""CLI frontend module for anyconfig.
"""
from __future__ import absolute_import, print_function
import argparse
import codecs
import locale
import logging
import os
import sys
import anyconfig.api as API
import anyconfig.compat
import anyconfig.globals
import anyconfig.parser
import anyconfig.utils
_ENCODING = locale.getdefaultlocale()[1] or 'UTF-8'
logging.basicConfig(format="%(levelname)s: %(message)s")
LOGGER = logging.getLogger("anyconfig")
LOGGER.addHandler(logging.StreamHandler())
if anyconfig.compat.IS_PYTHON_3:
import io
_ENCODING = _ENCODING.lower()
# TODO: What should be done for an error, "AttributeError: '_io.StringIO'
# object has no attribute 'buffer'"?
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=_ENCODING)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=_ENCODING)
except AttributeError:
pass
else:
sys.stdout = codecs.getwriter(_ENCODING)(sys.stdout)
sys.stderr = codecs.getwriter(_ENCODING)(sys.stderr)
USAGE = """\
%(prog)s [Options...] CONF_PATH_OR_PATTERN_0 [CONF_PATH_OR_PATTERN_1 ..]
Examples:
%(prog)s --list # -> Supported config types: configobj, ini, json, ...
# Merge and/or convert input config to output config [file]
%(prog)s -I yaml -O yaml /etc/xyz/conf.d/a.conf
%(prog)s -I yaml '/etc/xyz/conf.d/*.conf' -o xyz.conf --otype json
%(prog)s '/etc/xyz/conf.d/*.json' -o xyz.yml \\
--atype json -A '{"obsoletes": "syscnf", "conflicts": "syscnf-old"}'
%(prog)s '/etc/xyz/conf.d/*.json' -o xyz.yml \\
-A obsoletes:syscnf;conflicts:syscnf-old
%(prog)s /etc/foo.json /etc/foo/conf.d/x.json /etc/foo/conf.d/y.json
%(prog)s '/etc/foo.d/*.json' -M noreplace
# Query/Get/set part of input config
%(prog)s '/etc/foo.d/*.json' --query 'locs[?state == 'T'].name | sort(@)'
%(prog)s '/etc/foo.d/*.json' --get a.b.c
%(prog)s '/etc/foo.d/*.json' --set a.b.c=1
# Validate with JSON schema or generate JSON schema:
%(prog)s --validate -S foo.conf.schema.yml '/etc/foo.d/*.xml'
%(prog)s --gen-schema '/etc/foo.d/*.xml' -o foo.conf.schema.yml"""
DEFAULTS = dict(loglevel=0, list=False, output=None, itype=None,
otype=None, atype=None, merge=API.MS_DICTS,
ignore_missing=False, template=False, env=False,
schema=None, validate=False, gen_schema=False)
def to_log_level(level):
"""
:param level: Logging level in int = 0 .. 2
>>> to_log_level(0) == logging.WARN
True
>>> to_log_level(5) # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
Traceback (most recent call last):
...
ValueError: wrong log level passed: 5
>>>
"""
if not (level >= 0 and level < 3):
raise ValueError("wrong log level passed: " + str(level))
return [logging.WARN, logging.INFO, logging.DEBUG][level]
_ATYPE_HELP_FMT = """\
Explicitly select type of argument to provide configs from %s.
If this option is not set, original parser is used: 'K:V' will become {K: V},
'K:V_0,V_1,..' will become {K: [V_0, V_1, ...]}, and 'K_0:V_0;K_1:V_1' will
become {K_0: V_0, K_1: V_1} (where the tyep of K is str, type of V is one of
Int, str, etc."""
_QUERY_HELP = ("Query with JMESPath expression language. See "
"http://jmespath.org for more about JMESPath expression. "
"This option is not used with --get option at the same time. "
"Please note that python module to support JMESPath "
"expression (https://pypi.python.org/pypi/jmespath/) is "
"required to use this option")
_GET_HELP = ("Specify key path to get part of config, for example, "
"'--get a.b.c' to config {'a': {'b': {'c': 0, 'd': 1}}} "
"gives 0 and '--get a.b' to the same config gives "
"{'c': 0, 'd': 1}. Path expression can be JSON Pointer "
"expression (http://tools.ietf.org/html/rfc6901) such like "
"'', '/a~1b', '/m~0n'. "
"This option is not used with --query option at the same time. ")
_SET_HELP = ("Specify key path to set (update) part of config, for "
"example, '--set a.b.c=1' to a config {'a': {'b': {'c': 0, "
"'d': 1}}} gives {'a': {'b': {'c': 1, 'd': 1}}}.")
def make_parser(defaults=None):
"""
:param defaults: Default option values
"""
if defaults is None:
defaults = DEFAULTS
ctypes = API.list_types()
ctypes_s = ", ".join(ctypes)
type_help = "Select type of %s config files from " + \
ctypes_s + " [Automatically detected by file ext]"
mts = API.MERGE_STRATEGIES
mts_s = ", ".join(mts)
mt_help = "Select strategy to merge multiple configs from " + \
mts_s + " [%(merge)s]" % defaults
parser = argparse.ArgumentParser(usage=USAGE)
parser.set_defaults(**defaults)
parser.add_argument("inputs", type=str, nargs='*', help="Input files")
parser.add_argument("--version", action="version",
version="%%(prog)s %s" % anyconfig.globals.VERSION)
lpog = parser.add_argument_group("List specific options")
lpog.add_argument("-L", "--list", action="store_true",
help="List supported config types")
spog = parser.add_argument_group("Schema specific options")
spog.add_argument("--validate", action="store_true",
help="Only validate input files and do not output. "
"You must specify schema file with -S/--schema "
"option.")
spog.add_argument("--gen-schema", action="store_true",
help="Generate JSON schema for givne config file[s] "
"and output it instead of (merged) configuration.")
gspog = parser.add_argument_group("Query/Get/set options")
gspog.add_argument("-Q", "--query", help=_QUERY_HELP)
gspog.add_argument("--get", help=_GET_HELP)
gspog.add_argument("--set", help=_SET_HELP)
parser.add_argument("-o", "--output", help="Output file path")
parser.add_argument("-I", "--itype", choices=ctypes, metavar="ITYPE",
help=(type_help % "Input"))
parser.add_argument("-O", "--otype", choices=ctypes, metavar="OTYPE",
help=(type_help % "Output"))
parser.add_argument("-M", "--merge", choices=mts, metavar="MERGE",
help=mt_help)
parser.add_argument("-A", "--args", help="Argument configs to override")
parser.add_argument("--atype", choices=ctypes, metavar="ATYPE",
help=_ATYPE_HELP_FMT % ctypes_s)
cpog = parser.add_argument_group("Common options")
cpog.add_argument("-x", "--ignore-missing", action="store_true",
help="Ignore missing input files")
cpog.add_argument("-T", "--template", action="store_true",
help="Enable template config support")
cpog.add_argument("-E", "--env", action="store_true",
help="Load configuration defaults from "
"environment values")
cpog.add_argument("-S", "--schema", help="Specify Schema file[s] path")
cpog.add_argument("-v", "--verbose", action="count", dest="loglevel",
help="Verbose mode; -v or -vv (more verbose)")
return parser
def _exit_with_output(content, exit_code=0):
"""
Exit the program with printing out messages.
:param content: content to print out
:param exit_code: Exit code
"""
(sys.stdout if exit_code == 0 else sys.stderr).write(content + os.linesep)
sys.exit(exit_code)
def _parse_args(argv):
"""
Show supported config format types or usage.
:param argv: Argument list to parse or None (sys.argv will be set).
:return: argparse.Namespace object or None (exit before return)
"""
parser = make_parser()
args = parser.parse_args(argv)
LOGGER.setLevel(to_log_level(args.loglevel))
if not args.inputs:
if args.list:
tlist = ", ".join(API.list_types())
_exit_with_output("Supported config types: " + tlist)
elif args.env:
cnf = os.environ.copy()
_output_result(cnf, args.output, args.otype or "json", None, None)
sys.exit(0)
else:
parser.print_usage()
sys.exit(1)
if args.validate and args.schema is None:
_exit_with_output("--validate option requires --scheme option", 1)
return args
def _exit_if_load_failure(cnf, msg):
"""
:param cnf: Loaded configuration object or None indicates load failure
:param msg: Message to print out if failure
"""
if cnf is None:
_exit_with_output(msg, 1)
def _do_get(cnf, get_path):
"""
:param cnf: Configuration object to print out
:param get_path: key path given in --get option
:return: updated Configuration object if no error
"""
(cnf, err) = API.get(cnf, get_path)
if cnf is None: # Failed to get the result.
_exit_with_output("Failed to get result: err=%s" % err, 1)
return cnf
def _output_type_by_input_path(inpaths, itype, fmsg):
"""
:param inpaths: List of input file paths
:param itype: Input type or None
:param fmsg: message if it cannot detect otype by `inpath`
:return: Output type :: str
"""
msg = ("Specify inpath and/or outpath type[s] with -I/--itype "
"or -O/--otype option explicitly")
if itype is None:
try:
otype = API.find_loader(inpaths[0]).type()
except API.UnknownFileTypeError:
_exit_with_output((fmsg % inpaths[0]) + msg, 1)
except (ValueError, IndexError):
_exit_with_output(msg, 1)
else:
otype = itype
return otype
def _try_dump(cnf, outpath, otype, fmsg):
"""
:param cnf: Configuration object to print out
:param outpath: Output file path or None
:param otype: Output type or None
:param fmsg: message if it cannot detect otype by `inpath`
"""
try:
API.dump(cnf, outpath, otype)
except API.UnknownFileTypeError:
_exit_with_output(fmsg % outpath, 1)
except API.UnknownProcessorTypeError:
_exit_with_output("Invalid output type '%s'" % otype, 1)
def _output_result(cnf, outpath, otype, inpaths, itype):
"""
:param cnf: Configuration object to print out
:param outpath: Output file path or None
:param otype: Output type or None
:param inpaths: List of input file paths
:param itype: Input type or None
"""
fmsg = ("Uknown file type and cannot detect appropriate backend "
"from its extension, '%s'")
if not anyconfig.utils.is_dict_like(cnf):
_exit_with_output(str(cnf)) # Print primitive types as it is.
if not outpath or outpath == "-":
outpath = sys.stdout
if otype is None:
otype = _output_type_by_input_path(inpaths, itype, fmsg)
_try_dump(cnf, outpath, otype, fmsg)
def _load_diff(args):
"""
:param args: :class:`~argparse.Namespace` object
"""
try:
diff = API.load(args.inputs, args.itype,
ac_ignore_missing=args.ignore_missing,
ac_merge=args.merge,
ac_template=args.template,
ac_schema=args.schema)
except API.UnknownProcessorTypeError:
_exit_with_output("Wrong input type '%s'" % args.itype, 1)
except API.UnknownFileTypeError:
_exit_with_output("No appropriate backend was found for given file "
"'%s'" % args.itype, 1)
_exit_if_load_failure(diff,
"Failed to load: args=%s" % ", ".join(args.inputs))
return diff
def _do_filter(cnf, args):
"""
:param cnf: Mapping object represents configuration data
:param args: :class:`~argparse.Namespace` object
:return: `cnf` may be updated
"""
if args.query:
cnf = API.query(cnf, args.query)
elif args.get:
cnf = _do_get(cnf, args.get)
elif args.set:
(key, val) = args.set.split('=')
API.set_(cnf, key, anyconfig.parser.parse(val))
return cnf
def main(argv=None):
"""
:param argv: Argument list to parse or None (sys.argv will be set).
"""
args = _parse_args((argv if argv else sys.argv)[1:])
cnf = os.environ.copy() if args.env else {}
diff = _load_diff(args)
if cnf:
API.merge(cnf, diff)
else:
cnf = diff
if args.args:
diff = anyconfig.parser.parse(args.args)
API.merge(cnf, diff)
if args.validate:
_exit_with_output("Validation succeds")
cnf = API.gen_schema(cnf) if args.gen_schema else _do_filter(cnf, args)
_output_result(cnf, args.output, args.otype, args.inputs, args.itype)
if __name__ == '__main__':
main(sys.argv)
# vim:sw=4:ts=4:et: