1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
21
from six.moves import zip
23
from charmhelpers.core import unitdata
26
class OutputFormatter(object):
27
def __init__(self, outfile=sys.stdout):
36
self.outfile = outfile
38
def add_arguments(self, argument_parser):
39
formatgroup = argument_parser.add_mutually_exclusive_group()
40
choices = self.supported_formats
41
formatgroup.add_argument("--format", metavar='FMT',
42
help="Select output format for returned data, "
43
"where FMT is one of: {}".format(choices),
44
choices=choices, default='raw')
45
for fmt in self.formats:
46
fmtfunc = getattr(self, fmt)
47
formatgroup.add_argument("-{}".format(fmt[0]),
48
"--{}".format(fmt), action='store_const',
49
const=fmt, dest='format',
53
def supported_formats(self):
56
def raw(self, output):
57
"""Output data as raw string (default)"""
58
if isinstance(output, (list, tuple)):
59
output = '\n'.join(map(str, output))
60
self.outfile.write(str(output))
63
"""Output data as a nicely-formatted python data structure"""
65
pprint.pprint(output, stream=self.outfile)
67
def json(self, output):
68
"""Output data in JSON format"""
70
json.dump(output, self.outfile)
72
def yaml(self, output):
73
"""Output data in YAML format"""
75
yaml.safe_dump(output, self.outfile)
77
def csv(self, output):
78
"""Output data as excel-compatible CSV"""
80
csvwriter = csv.writer(self.outfile)
81
csvwriter.writerows(output)
83
def tab(self, output):
84
"""Output data in excel-compatible tab-delimited format"""
86
csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
87
csvwriter.writerows(output)
89
def format_output(self, output, fmt='raw'):
90
fmtfunc = getattr(self, fmt)
94
class CommandLine(object):
95
argument_parser = None
101
if not self.argument_parser:
102
self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
103
if not self.formatter:
104
self.formatter = OutputFormatter()
105
self.formatter.add_arguments(self.argument_parser)
106
if not self.subparsers:
107
self.subparsers = self.argument_parser.add_subparsers(help='Commands')
109
def subcommand(self, command_name=None):
111
Decorate a function as a subcommand. Use its arguments as the
112
command-line arguments"""
113
def wrapper(decorated):
114
cmd_name = command_name or decorated.__name__
115
subparser = self.subparsers.add_parser(cmd_name,
116
description=decorated.__doc__)
117
for args, kwargs in describe_arguments(decorated):
118
subparser.add_argument(*args, **kwargs)
119
subparser.set_defaults(func=decorated)
123
def test_command(self, decorated):
125
Subcommand is a boolean test function, so bool return values should be
126
converted to a 0/1 exit code.
128
decorated._cli_test_command = True
131
def no_output(self, decorated):
133
Subcommand is not expected to return a value, so don't print a spurious None.
135
decorated._cli_no_output = True
138
def subcommand_builder(self, command_name, description=None):
140
Decorate a function that builds a subcommand. Builders should accept a
141
single argument (the subparser instance) and return the function to be
142
run as the command."""
143
def wrapper(decorated):
144
subparser = self.subparsers.add_parser(command_name)
145
func = decorated(subparser)
146
subparser.set_defaults(func=func)
147
subparser.description = description or func.__doc__
151
"Run cli, processing arguments and executing subcommands."
152
arguments = self.argument_parser.parse_args()
153
argspec = inspect.getargspec(arguments.func)
156
for arg in argspec.args:
157
vargs.append(getattr(arguments, arg))
159
vargs.extend(getattr(arguments, argspec.varargs))
161
for kwarg in argspec.keywords.items():
162
kwargs[kwarg] = getattr(arguments, kwarg)
163
output = arguments.func(*vargs, **kwargs)
164
if getattr(arguments.func, '_cli_test_command', False):
165
self.exit_code = 0 if output else 1
167
if getattr(arguments.func, '_cli_no_output', False):
169
self.formatter.format_output(output, arguments.format)
174
cmdline = CommandLine()
177
def describe_arguments(func):
179
Analyze a function's signature and return a data structure suitable for
180
passing in as arguments to an argparse parser's add_argument() method."""
182
argspec = inspect.getargspec(func)
183
# we should probably raise an exception somewhere if func includes **kwargs
185
positional_args = argspec.args[:-len(argspec.defaults)]
186
keyword_names = argspec.args[-len(argspec.defaults):]
187
for arg, default in zip(keyword_names, argspec.defaults):
188
yield ('--{}'.format(arg),), {'default': default}
190
positional_args = argspec.args
192
for arg in positional_args:
195
yield (argspec.varargs,), {'nargs': '*'}